@songsid/agend 2.1.4-beta.7 → 2.1.4-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel/adapters/discord.js +7 -0
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-server.js +6 -20
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/config-validator.js +3 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +50 -0
- package/dist/daemon.js +173 -34
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +60 -1
- package/dist/fleet-manager.js +416 -195
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/backend-providers/SKILL.md +114 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +3 -1
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +52 -0
- package/dist/general-knowledge/skills/development-workflow/SKILL.md +28 -0
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -0
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +1 -0
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -0
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +1 -0
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +1 -0
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -0
- package/dist/general-knowledge/skills/scheduling/SKILL.md +1 -0
- package/dist/general-knowledge/skills/session-management/SKILL.md +1 -0
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -0
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +30 -0
- package/dist/instance-lifecycle.d.ts +2 -0
- package/dist/instance-lifecycle.js +1 -0
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +12 -0
- package/dist/instructions.js +33 -0
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +10 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-schemas.d.ts +1 -0
- package/dist/outbound-schemas.js +1 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/tool-progress.d.ts +40 -0
- package/dist/tool-progress.js +289 -0
- package/dist/tool-progress.js.map +1 -0
- package/dist/topic-commands.d.ts +15 -0
- package/dist/topic-commands.js +56 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.d.ts +15 -2
- package/dist/transcript-monitor.js +63 -17
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +106 -0
- package/dist/transcript-sources.js +428 -0
- package/dist/transcript-sources.js.map +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/workflow-templates/default.md +2 -1
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -135,6 +135,8 @@ const CANCEL_BTN_LEDGER_FILE = "cancel-buttons.json";
|
|
|
135
135
|
* exactly one progress message per turn, and it is the cancel button itself.
|
|
136
136
|
*/
|
|
137
137
|
const PROGRESS_UPDATE_INTERVAL_MS = 60_000;
|
|
138
|
+
/** Floor between tool-progress-driven bubble edits (Telegram flood safety). */
|
|
139
|
+
const TOOL_PROGRESS_EDIT_MIN_MS = 4_000;
|
|
138
140
|
/** Elapsed time is only shown once work has clearly outlasted a quick answer. */
|
|
139
141
|
/**
|
|
140
142
|
* Default delay before the button starts showing elapsed time. Configurable via
|
|
@@ -253,6 +255,8 @@ export class FleetManager {
|
|
|
253
255
|
replyDeduper = new ReplyDeduper();
|
|
254
256
|
/** instanceName → what it is doing right now, when the backend can tell us. */
|
|
255
257
|
instanceActivity = new Map();
|
|
258
|
+
/** instanceName → this turn's tool list (multi-line), for the bubble. */
|
|
259
|
+
instanceProgress = new Map();
|
|
256
260
|
/** instanceName → tail of deliveries waiting for its IPC to come back. */
|
|
257
261
|
ipcWaitTails = new Map();
|
|
258
262
|
/** instanceName → restart currently executing; concurrent callers join it. */
|
|
@@ -1074,19 +1078,34 @@ export class FleetManager {
|
|
|
1074
1078
|
this.logger.info({ name }, "Instance already running, skipping");
|
|
1075
1079
|
return;
|
|
1076
1080
|
}
|
|
1081
|
+
const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
1077
1082
|
if (config.general_topic) {
|
|
1078
1083
|
// antigravity (agy) does not read MCP instructions — fleet context and
|
|
1079
1084
|
// routing instructions are not injected, so it cannot act as a dispatcher.
|
|
1080
|
-
const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
1081
1085
|
if (backend === "antigravity") {
|
|
1082
1086
|
this.logger.warn({ name }, "antigravity backend does not support MCP instructions — general dispatcher will not work correctly");
|
|
1083
1087
|
this.notifyInstanceTopic(name, "⚠️ antigravity backend is not supported for General instances (no MCP instructions injection). Switch to claude-code or kiro-cli.");
|
|
1084
1088
|
}
|
|
1085
|
-
this.ensureGeneralInstructions(config.working_directory,
|
|
1089
|
+
this.ensureGeneralInstructions(config.working_directory, backend, name);
|
|
1090
|
+
}
|
|
1091
|
+
else if (kind === "fleet-topic") {
|
|
1092
|
+
// Workers get only role-eligible on-demand skills. Classic instances are
|
|
1093
|
+
// deliberately excluded: their workspace and conversation lifecycle are
|
|
1094
|
+
// managed independently from fleet-topic workers.
|
|
1095
|
+
try {
|
|
1096
|
+
const skillsWorkDir = this.resolveKnowledgeWorkDir(config.working_directory, backend, name);
|
|
1097
|
+
this.syncRoleSkills(skillsWorkDir, backend, "worker");
|
|
1098
|
+
}
|
|
1099
|
+
catch (err) {
|
|
1100
|
+
// Skill publishing is additive. A read-only or temporarily unavailable
|
|
1101
|
+
// workspace must not turn an otherwise valid worker startup into a
|
|
1102
|
+
// fleet outage.
|
|
1103
|
+
this.logger.warn({ err, name, backend }, "Failed to sync worker skills — continuing startup");
|
|
1104
|
+
}
|
|
1086
1105
|
}
|
|
1087
1106
|
await this.lifecycle.start(name, config, topicMode, {
|
|
1088
1107
|
kind,
|
|
1089
|
-
backend
|
|
1108
|
+
backend,
|
|
1090
1109
|
model: this.resolveInstanceModel(name).display,
|
|
1091
1110
|
});
|
|
1092
1111
|
// Only clear a stale process status after a real start succeeded. Clearing
|
|
@@ -1661,7 +1680,7 @@ export class FleetManager {
|
|
|
1661
1680
|
const generalDir = join(getAgendHome(), name);
|
|
1662
1681
|
mkdirSync(generalDir, { recursive: true });
|
|
1663
1682
|
const backendName = fleet.defaults.backend ?? "claude-code";
|
|
1664
|
-
this.ensureGeneralInstructions(generalDir, backendName);
|
|
1683
|
+
this.ensureGeneralInstructions(generalDir, backendName, name);
|
|
1665
1684
|
fleet.instances[name] = {
|
|
1666
1685
|
...DEFAULT_INSTANCE_CONFIG,
|
|
1667
1686
|
working_directory: generalDir,
|
|
@@ -2220,6 +2239,26 @@ export class FleetManager {
|
|
|
2220
2239
|
const result = await this.topicCommands.sendCompact(name);
|
|
2221
2240
|
await data.respond(result);
|
|
2222
2241
|
}
|
|
2242
|
+
else if (data.command === "steer") {
|
|
2243
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
2244
|
+
if (!name) {
|
|
2245
|
+
await data.respond(t("classic.no_agent"));
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
const steerText = String(data.options?.message ?? "").trim();
|
|
2249
|
+
if (!steerText) {
|
|
2250
|
+
await data.respond(t("steer.usage"));
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
// chat_id/message_id stay empty: a slash interaction has no channel
|
|
2254
|
+
// message to react to, and an empty chat_id keeps updateLastChat from
|
|
2255
|
+
// rerouting the instance's replies to the slash context.
|
|
2256
|
+
const result = this.topicCommands.sendSteer(name, steerText, {
|
|
2257
|
+
chatId: "", messageId: "", username: data.username ?? "user",
|
|
2258
|
+
userId: data.userId ?? "", threadId: undefined, adapterId, source: "discord",
|
|
2259
|
+
});
|
|
2260
|
+
await data.respond(result);
|
|
2261
|
+
}
|
|
2223
2262
|
else if (data.command === "clear") {
|
|
2224
2263
|
await this.handleClearSlash(data, adapterId);
|
|
2225
2264
|
}
|
|
@@ -2344,6 +2383,26 @@ export class FleetManager {
|
|
|
2344
2383
|
const result = await this.topicCommands.sendCompact(name);
|
|
2345
2384
|
await data.respond(result);
|
|
2346
2385
|
}
|
|
2386
|
+
else if (data.command === "steer") {
|
|
2387
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
2388
|
+
if (!name) {
|
|
2389
|
+
await data.respond(t("classic.no_agent"));
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
const steerText = String(data.options?.message ?? "").trim();
|
|
2393
|
+
if (!steerText) {
|
|
2394
|
+
await data.respond(t("steer.usage"));
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
// chat_id/message_id stay empty: a slash interaction has no channel
|
|
2398
|
+
// message to react to, and an empty chat_id keeps updateLastChat from
|
|
2399
|
+
// rerouting the instance's replies to the slash context.
|
|
2400
|
+
const result = this.topicCommands.sendSteer(name, steerText, {
|
|
2401
|
+
chatId: "", messageId: "", username: data.username ?? "user",
|
|
2402
|
+
userId: data.userId ?? "", threadId: undefined, adapterId, source: "discord",
|
|
2403
|
+
});
|
|
2404
|
+
await data.respond(result);
|
|
2405
|
+
}
|
|
2347
2406
|
}, this.logger, "adapter.slash_command"));
|
|
2348
2407
|
await this.topicCommands.registerBotCommands().catch(e => this.logger.warn({ err: e }, "registerBotCommands failed (non-fatal)"));
|
|
2349
2408
|
// Background-probe each backend's CLI env (version/models) → cli-env cache.
|
|
@@ -2626,6 +2685,26 @@ export class FleetManager {
|
|
|
2626
2685
|
const result = await this.topicCommands.sendCompact(name);
|
|
2627
2686
|
await data.respond(result);
|
|
2628
2687
|
}
|
|
2688
|
+
else if (data.command === "steer") {
|
|
2689
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
2690
|
+
if (!name) {
|
|
2691
|
+
await data.respond(t("classic.no_agent"));
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
const steerText = String(data.options?.message ?? "").trim();
|
|
2695
|
+
if (!steerText) {
|
|
2696
|
+
await data.respond(t("steer.usage"));
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
// chat_id/message_id stay empty: a slash interaction has no channel
|
|
2700
|
+
// message to react to, and an empty chat_id keeps updateLastChat from
|
|
2701
|
+
// rerouting the instance's replies to the slash context.
|
|
2702
|
+
const result = this.topicCommands.sendSteer(name, steerText, {
|
|
2703
|
+
chatId: "", messageId: "", username: data.username ?? "user",
|
|
2704
|
+
userId: data.userId ?? "", threadId: undefined, adapterId, source: "discord",
|
|
2705
|
+
});
|
|
2706
|
+
await data.respond(result);
|
|
2707
|
+
}
|
|
2629
2708
|
}, this.logger, `adapter[${adapterId}].slash_command`));
|
|
2630
2709
|
await adapter.start();
|
|
2631
2710
|
if (channelConfig.group_id) {
|
|
@@ -2743,6 +2822,9 @@ export class FleetManager {
|
|
|
2743
2822
|
else if (msg.type === "instance_activity") {
|
|
2744
2823
|
this.cacheInstanceActivity(name, msg.activity);
|
|
2745
2824
|
}
|
|
2825
|
+
else if (msg.type === "instance_progress") {
|
|
2826
|
+
this.cacheInstanceProgress(name, msg.progress || null);
|
|
2827
|
+
}
|
|
2746
2828
|
else if (msg.type === "instance_state" || msg.type === "instance_state_response") {
|
|
2747
2829
|
this.cacheInstanceExecutionState(name, msg);
|
|
2748
2830
|
if (msg.type === "instance_state_response") {
|
|
@@ -3190,6 +3272,24 @@ export class FleetManager {
|
|
|
3190
3272
|
await msgAdapter?.sendText(chatId, result);
|
|
3191
3273
|
return;
|
|
3192
3274
|
}
|
|
3275
|
+
// /steer — interject into the running turn. Not admin-gated: anyone who
|
|
3276
|
+
// can talk to this agent can send it a message; steer only changes when
|
|
3277
|
+
// it lands, and it keeps the full [user:] formatting (unlike /raw).
|
|
3278
|
+
if (text === "/steer" || text.startsWith("/steer ") || text.startsWith("/steer@")) {
|
|
3279
|
+
const steerName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
|
|
3280
|
+
if (!steerName) {
|
|
3281
|
+
await msgAdapter?.sendText(chatId, t("classic.no_agent_start"));
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
const steerContent = text.replace(/^\/steer(@\S+)?/, "").trim();
|
|
3285
|
+
if (!steerContent) {
|
|
3286
|
+
await msgAdapter?.sendText(chatId, t("steer.usage"));
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
const result = this.topicCommands.sendSteer(steerName, steerContent, msg);
|
|
3290
|
+
await msgAdapter?.sendText(chatId, result);
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3193
3293
|
// Handle /clear command (admin only) — unlike /compact this starts a
|
|
3194
3294
|
// fresh conversation and intentionally discards the current history.
|
|
3195
3295
|
if (text === "/clear" || text.startsWith("/clear@")) {
|
|
@@ -5115,6 +5215,80 @@ export class FleetManager {
|
|
|
5115
5215
|
else
|
|
5116
5216
|
this.instanceActivity.delete(name);
|
|
5117
5217
|
}
|
|
5218
|
+
/**
|
|
5219
|
+
* Cache the turn's tool-progress list and push it into the instance's live
|
|
5220
|
+
* bubble, coalesced so a burst of tool events cannot flood the Bot API
|
|
5221
|
+
* (Telegram's flood limit is per-chat across ALL forum topics, so edits are
|
|
5222
|
+
* rate-limited per bubble AND ride behind the daemon-side 3s coalescer).
|
|
5223
|
+
*/
|
|
5224
|
+
cacheInstanceProgress(name, progress) {
|
|
5225
|
+
if (progress)
|
|
5226
|
+
this.instanceProgress.set(name, progress);
|
|
5227
|
+
else
|
|
5228
|
+
this.instanceProgress.delete(name);
|
|
5229
|
+
for (const entry of this.cancelButtons.values()) {
|
|
5230
|
+
if (entry.instanceName === name)
|
|
5231
|
+
this.scheduleProgressEdit(entry);
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
/** At most one progress-driven edit per bubble per TOOL_PROGRESS_EDIT_MIN_MS. */
|
|
5235
|
+
scheduleProgressEdit(entry) {
|
|
5236
|
+
const since = Date.now() - (entry.lastProgressEditAt ?? 0);
|
|
5237
|
+
if (since >= TOOL_PROGRESS_EDIT_MIN_MS) {
|
|
5238
|
+
this.refreshBubble(entry);
|
|
5239
|
+
return;
|
|
5240
|
+
}
|
|
5241
|
+
if (entry.progressEditTimer)
|
|
5242
|
+
return; // trailing edit already scheduled
|
|
5243
|
+
entry.progressEditTimer = setTimeout(() => {
|
|
5244
|
+
entry.progressEditTimer = undefined;
|
|
5245
|
+
this.refreshBubble(entry);
|
|
5246
|
+
}, TOOL_PROGRESS_EDIT_MIN_MS - since);
|
|
5247
|
+
entry.progressEditTimer.unref?.();
|
|
5248
|
+
}
|
|
5249
|
+
/**
|
|
5250
|
+
* The ONE composer for the bubble text. Both writers — the elapsed-time
|
|
5251
|
+
* ticker and the tool-progress push — go through here; two independent
|
|
5252
|
+
* renderers editing the same message is how the progress list used to get
|
|
5253
|
+
* wiped by the next elapsed tick (#528 trap 2).
|
|
5254
|
+
*/
|
|
5255
|
+
composeBubbleText(entry) {
|
|
5256
|
+
return FleetManager.bubbleText(Date.now() - (entry.startedAt ?? Date.now()), this.instanceActivity.get(entry.instanceName), this.progressMinElapsedMs(), this.instanceProgress.get(entry.instanceName));
|
|
5257
|
+
}
|
|
5258
|
+
/** Pure composition of header + tool list, exposed for tests. */
|
|
5259
|
+
static bubbleText(elapsedMs, activity, minElapsedMs, progress) {
|
|
5260
|
+
const header = FleetManager.progressText(elapsedMs,
|
|
5261
|
+
// The single-line activity detail is redundant once a tool list exists.
|
|
5262
|
+
progress ? undefined : activity, minElapsedMs);
|
|
5263
|
+
return progress ? `${header}\n${progress}` : header;
|
|
5264
|
+
}
|
|
5265
|
+
/** Recompose and edit the bubble in place; skips when nothing changed. */
|
|
5266
|
+
refreshBubble(entry) {
|
|
5267
|
+
if (!this.cancelButtons.has(entry.messageId)) {
|
|
5268
|
+
clearInterval(entry.progressTimer);
|
|
5269
|
+
return;
|
|
5270
|
+
}
|
|
5271
|
+
const text = this.composeBubbleText(entry);
|
|
5272
|
+
if (text === entry.lastProgressText)
|
|
5273
|
+
return; // nothing changed — skip the API call
|
|
5274
|
+
const adapter = this.getAdapterForInstance(entry.instanceName) ?? this.adapter;
|
|
5275
|
+
if (!adapter?.editAlert)
|
|
5276
|
+
return;
|
|
5277
|
+
entry.lastProgressText = text;
|
|
5278
|
+
entry.lastProgressEditAt = Date.now();
|
|
5279
|
+
adapter.editAlert(entry.chatId, entry.messageId, {
|
|
5280
|
+
type: "cancel",
|
|
5281
|
+
instanceName: entry.instanceName,
|
|
5282
|
+
message: text,
|
|
5283
|
+
choices: [{ id: `cancel:${entry.instanceName}`, label: t("cancel.button") }],
|
|
5284
|
+
}, entry.threadId ? { threadId: entry.threadId } : undefined)
|
|
5285
|
+
.catch(err => {
|
|
5286
|
+
// A failed progress edit must never escalate: the button still works and
|
|
5287
|
+
// the next tick retries. Common causes are a deleted message or a
|
|
5288
|
+
// rate limit.
|
|
5289
|
+
this.logger.debug({ err, instanceName: entry.instanceName }, "Progress edit failed");
|
|
5290
|
+
});
|
|
5291
|
+
}
|
|
5118
5292
|
/**
|
|
5119
5293
|
* Refresh the button's text in place while the instance keeps working.
|
|
5120
5294
|
*
|
|
@@ -5132,31 +5306,7 @@ export class FleetManager {
|
|
|
5132
5306
|
return PROGRESS_MIN_ELAPSED_MS;
|
|
5133
5307
|
}
|
|
5134
5308
|
startProgressTicker(entry) {
|
|
5135
|
-
const tick = () =>
|
|
5136
|
-
if (!this.cancelButtons.has(entry.messageId)) {
|
|
5137
|
-
clearInterval(entry.progressTimer);
|
|
5138
|
-
return;
|
|
5139
|
-
}
|
|
5140
|
-
const text = FleetManager.progressText(Date.now() - (entry.startedAt ?? Date.now()), this.instanceActivity.get(entry.instanceName), this.progressMinElapsedMs());
|
|
5141
|
-
if (text === entry.lastProgressText)
|
|
5142
|
-
return; // nothing changed — skip the API call
|
|
5143
|
-
const adapter = this.getAdapterForInstance(entry.instanceName) ?? this.adapter;
|
|
5144
|
-
if (!adapter?.editAlert)
|
|
5145
|
-
return;
|
|
5146
|
-
entry.lastProgressText = text;
|
|
5147
|
-
adapter.editAlert(entry.chatId, entry.messageId, {
|
|
5148
|
-
type: "cancel",
|
|
5149
|
-
instanceName: entry.instanceName,
|
|
5150
|
-
message: text,
|
|
5151
|
-
choices: [{ id: `cancel:${entry.instanceName}`, label: t("cancel.button") }],
|
|
5152
|
-
}, entry.threadId ? { threadId: entry.threadId } : undefined)
|
|
5153
|
-
.catch(err => {
|
|
5154
|
-
// A failed progress edit must never escalate: the button still works and
|
|
5155
|
-
// the next tick retries. Common causes are a deleted message or a
|
|
5156
|
-
// rate limit.
|
|
5157
|
-
this.logger.debug({ err, instanceName: entry.instanceName }, "Progress edit failed");
|
|
5158
|
-
});
|
|
5159
|
-
};
|
|
5309
|
+
const tick = () => this.refreshBubble(entry);
|
|
5160
5310
|
entry.progressTimer = setInterval(tick, PROGRESS_UPDATE_INTERVAL_MS);
|
|
5161
5311
|
entry.progressTimer.unref?.();
|
|
5162
5312
|
// One extra tick right when the threshold passes, so a 30s threshold shows
|
|
@@ -5224,6 +5374,8 @@ export class FleetManager {
|
|
|
5224
5374
|
clearInterval(entry.idleCheckTimer);
|
|
5225
5375
|
if (entry.progressTimer)
|
|
5226
5376
|
clearInterval(entry.progressTimer);
|
|
5377
|
+
if (entry.progressEditTimer)
|
|
5378
|
+
clearTimeout(entry.progressEditTimer);
|
|
5227
5379
|
if (entry.replyGraceTimer)
|
|
5228
5380
|
clearTimeout(entry.replyGraceTimer);
|
|
5229
5381
|
this.cancelButtons.delete(entry.messageId);
|
|
@@ -5482,6 +5634,10 @@ export class FleetManager {
|
|
|
5482
5634
|
"gemini-cli": "GEMINI.md",
|
|
5483
5635
|
"opencode": "AGENTS.md",
|
|
5484
5636
|
"kiro-cli": ".kiro/steering/project.md",
|
|
5637
|
+
// Grok reads AGENTS.md project docs; agy reads .agents/agents.md — the
|
|
5638
|
+
// same files their writeConfig() appends fleet instructions to.
|
|
5639
|
+
"grok": "AGENTS.md",
|
|
5640
|
+
"antigravity": ".agents/agents.md",
|
|
5485
5641
|
"mock": "CLAUDE.md",
|
|
5486
5642
|
};
|
|
5487
5643
|
static GENERAL_INSTRUCTIONS = `# Fleet Coordinator
|
|
@@ -5491,146 +5647,33 @@ You route tasks, manage instances, enforce policies, and synthesize results.
|
|
|
5491
5647
|
Do NOT modify project files directly — delegate file changes to the project's instance.
|
|
5492
5648
|
You CAN write code snippets, explain code, and answer technical questions directly.
|
|
5493
5649
|
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
## Task Classification
|
|
5497
|
-
|
|
5498
|
-
Classify every incoming request before acting.
|
|
5499
|
-
|
|
5500
|
-
### Handle Directly (ALL conditions must be true)
|
|
5501
|
-
|
|
5502
|
-
- No file system access needed
|
|
5503
|
-
- No external execution needed
|
|
5504
|
-
- Answerable from static knowledge
|
|
5505
|
-
- ≤ 2 reasoning steps
|
|
5506
|
-
|
|
5507
|
-
Examples: Q&A, translation, fleet status queries, explaining a concept, writing code snippets.
|
|
5508
|
-
|
|
5509
|
-
### Delegate to 1 Instance
|
|
5510
|
-
|
|
5511
|
-
- Task scoped to a single project or repo
|
|
5512
|
-
- Requires file access, code changes, or execution
|
|
5513
|
-
|
|
5514
|
-
### Coordinate Multiple Instances
|
|
5515
|
-
|
|
5516
|
-
- Task spans multiple repos or domains
|
|
5517
|
-
- Requires outputs from one instance to feed into another
|
|
5518
|
-
- Benefits from parallel execution (max 3 instances per task)
|
|
5519
|
-
|
|
5520
|
-
-----
|
|
5521
|
-
|
|
5522
|
-
## Instance Discovery (in this order)
|
|
5523
|
-
1. list_teams() → reuse existing teams first
|
|
5524
|
-
2. list_instances() → find by working_directory, description, or tags
|
|
5525
|
-
3. describe_instance() → confirm capabilities before delegating
|
|
5526
|
-
4. create_instance() → only if no suitable instance exists
|
|
5527
|
-
|
|
5528
|
-
Rules: prefer reuse over creation. Do NOT create duplicates of running instances.
|
|
5529
|
-
|
|
5530
|
-
-----
|
|
5531
|
-
|
|
5532
|
-
## Delegation Protocol
|
|
5650
|
+
## Task Routing
|
|
5533
5651
|
|
|
5534
|
-
|
|
5652
|
+
- **Handle directly**: no file/exec access needed, answerable from knowledge, ≤2 reasoning steps (Q&A, translation, status queries, code snippets).
|
|
5653
|
+
- **Delegate to 1 instance**: scoped to one project/repo, needs file access or execution.
|
|
5654
|
+
- **Coordinate multiple**: spans repos, outputs feed each other, or parallel helps (max 3 per task).
|
|
5535
5655
|
|
|
5536
|
-
|
|
5537
|
-
2. Expected output — what to return and in what form
|
|
5538
|
-
3. Policy reminder — "Follow Development Workflow policy" (for code tasks)
|
|
5656
|
+
Instance discovery order: list_teams() → list_instances() → describe_instance() → create_instance() only if nothing fits. Prefer reuse; never duplicate a running instance.
|
|
5539
5657
|
|
|
5540
|
-
|
|
5658
|
+
## Reply Contract
|
|
5541
5659
|
|
|
5542
|
-
|
|
5543
|
-
- If a task has bounced 3 times, stop and solve locally or reduce scope
|
|
5544
|
-
|
|
5545
|
-
### Execution Strategy
|
|
5546
|
-
|
|
5547
|
-
Parallel — use only when tasks are independent with no shared state
|
|
5548
|
-
Sequential — use when one task's output feeds into the next
|
|
5549
|
-
|
|
5550
|
-
-----
|
|
5551
|
-
|
|
5552
|
-
## Result Handling
|
|
5553
|
-
|
|
5554
|
-
When an instance reports back, classify the outcome:
|
|
5555
|
-
|
|
5556
|
-
- Success → Summarize key results for user. Omit internal coordination noise.
|
|
5557
|
-
- Partial → State what succeeded, what remains, proposed next steps.
|
|
5558
|
-
- Failure → Retry up to 2 times. If still failing: try alternative instance, reduce scope, or return partial result clearly marked.
|
|
5559
|
-
- No response → Ping again after reasonable wait. If still silent: report to user with options.
|
|
5560
|
-
|
|
5561
|
-
### Output to User
|
|
5562
|
-
|
|
5563
|
-
Every final response to the user should contain:
|
|
5564
|
-
|
|
5565
|
-
- Result — the actual answer or deliverable
|
|
5566
|
-
- Gaps — anything incomplete or unresolved (omit if none)
|
|
5567
|
-
|
|
5568
|
-
-----
|
|
5569
|
-
|
|
5570
|
-
## Shared Decisions
|
|
5571
|
-
|
|
5572
|
-
Use post_decision() / list_decisions() for any choice that affects more than 1 instance, changes an API contract, introduces a new dependency, or alters deployment process.
|
|
5573
|
-
|
|
5574
|
-
When instances disagree, collect both viewpoints, make a decision, and record it via post_decision.
|
|
5575
|
-
|
|
5576
|
-
-----
|
|
5660
|
+
Every final response to the user contains: the result (the actual answer or deliverable) and gaps (anything incomplete — omit if none). Summarize instance reports; omit internal coordination noise.
|
|
5577
5661
|
|
|
5578
5662
|
## After Restart
|
|
5579
5663
|
|
|
5580
|
-
|
|
5581
|
-
1. list_instances() → rebuild fleet awareness
|
|
5582
|
-
2. list_teams() → restore team structure
|
|
5583
|
-
3. list_decisions() → reload policies and conventions
|
|
5584
|
-
|
|
5585
|
-
Only then handle incoming requests.
|
|
5586
|
-
|
|
5587
|
-
-----
|
|
5588
|
-
|
|
5589
|
-
## Development Workflow Policy
|
|
5590
|
-
|
|
5591
|
-
All code changes across the fleet should follow this workflow.
|
|
5592
|
-
The coordinator enforces compliance but does not perform these steps directly.
|
|
5593
|
-
Remind instances of this policy when delegating code tasks.
|
|
5594
|
-
|
|
5595
|
-
### Workflow Stages
|
|
5596
|
-
Design Proposed → Design Approved → Implementation → Submit for Review → Under Review → Approved → Merge
|
|
5597
|
-
|
|
5598
|
-
### Policy Rules
|
|
5599
|
-
|
|
5600
|
-
1. Design before code — developer sends design proposal to reviewer before implementation. Consensus required before proceeding.
|
|
5601
|
-
2. Challenger pairing — every code task should have a developer + reviewer. Reviewer actively questions decisions and finds risks.
|
|
5602
|
-
3. Verify by execution — backend/CLI changes must be tested by running them. Do not trust documentation alone.
|
|
5603
|
-
4. Independent review — every merge requires code review from someone other than the author.
|
|
5604
|
-
5. Root cause first — bug fixes require confirmed root cause before proposing a fix.
|
|
5605
|
-
6. Merge conditions: tests pass, reviewer approved, branch and worktree cleaned up.
|
|
5664
|
+
BEFORE processing any new messages: 1. list_instances() 2. list_teams() 3. list_decisions(). Only then handle requests.
|
|
5606
5665
|
|
|
5607
|
-
|
|
5666
|
+
## Playbooks (on-demand skills)
|
|
5608
5667
|
|
|
5609
|
-
|
|
5610
|
-
-
|
|
5611
|
-
-
|
|
5612
|
-
|
|
5613
|
-
-----
|
|
5614
|
-
|
|
5615
|
-
## Team Management
|
|
5616
|
-
|
|
5617
|
-
- Always check existing teams before creating new ones
|
|
5618
|
-
- Default to ephemeral teams (created for a specific task, dissolved after completion)
|
|
5619
|
-
- Clean up ephemeral teams and instances after task completion
|
|
5620
|
-
|
|
5621
|
-
-----
|
|
5622
|
-
|
|
5623
|
-
## Instance Configuration Tips
|
|
5624
|
-
|
|
5625
|
-
When users create specialized instances, suggest these configurations:
|
|
5626
|
-
|
|
5627
|
-
- **Reviewer instances**: Add \`pre_task_command: "/chat load reviewer-base"\` to reset context before each review, preventing influence from previous conversations.
|
|
5628
|
-
- **Collab mode**: For multi-bot channels, use \`/collab\` to enable @mention-based triggering.
|
|
5629
|
-
- **Cost control**: Set per-instance \`cost_guard\` for expensive backends.
|
|
5668
|
+
Detailed procedures live in your skills — consult them when the situation comes up rather than from memory:
|
|
5669
|
+
- **delegation-playbook** — delegation protocol, loop prevention, parallel vs sequential, result/failure handling, team management, instance configuration tips.
|
|
5670
|
+
- **development-workflow** — the fleet-wide code-change policy you enforce when delegating code tasks.
|
|
5671
|
+
Plus the operational skills (fleet-health, instance-lifecycle, scheduling, session-management, …).
|
|
5630
5672
|
`;
|
|
5631
5673
|
/** Ensure the general instance has its project instructions file + knowledge */
|
|
5632
|
-
ensureGeneralInstructions(workDir, backendName) {
|
|
5674
|
+
ensureGeneralInstructions(workDir, backendName, instanceName) {
|
|
5633
5675
|
const backend = backendName ?? "claude-code";
|
|
5676
|
+
workDir = this.resolveKnowledgeWorkDir(workDir, backend, instanceName);
|
|
5634
5677
|
const filename = FleetManager.INSTRUCTIONS_FILENAME[backend] ?? "CLAUDE.md";
|
|
5635
5678
|
const filePath = join(workDir, filename);
|
|
5636
5679
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
@@ -5638,60 +5681,238 @@ When users create specialized instances, suggest these configurations:
|
|
|
5638
5681
|
writeFileSync(filePath, FleetManager.GENERAL_INSTRUCTIONS, "utf-8");
|
|
5639
5682
|
this.logger.info({ filePath }, "Created general instance instructions file");
|
|
5640
5683
|
}
|
|
5641
|
-
// Sync bundled knowledge files to general's steering
|
|
5684
|
+
// Sync bundled knowledge files to general's steering and skills directories.
|
|
5642
5685
|
this.syncGeneralKnowledge(workDir, backend);
|
|
5643
5686
|
}
|
|
5644
|
-
/**
|
|
5687
|
+
/** Resolve the workspace path a backend actually uses before publishing knowledge. */
|
|
5688
|
+
resolveKnowledgeWorkDir(workDir, backend, instanceName) {
|
|
5689
|
+
// Some backends relocate their real cwd (agy moves hidden paths to
|
|
5690
|
+
// ~/agend-workspaces/<name>). Instructions and skills must land where the
|
|
5691
|
+
// CLI actually runs, not where fleet.yaml points.
|
|
5692
|
+
try {
|
|
5693
|
+
const resolved = createBackend(backend, join(getAgendHome(), "cli-env"))
|
|
5694
|
+
.resolveWorkingDirectory?.(workDir, instanceName);
|
|
5695
|
+
if (resolved)
|
|
5696
|
+
workDir = resolved;
|
|
5697
|
+
}
|
|
5698
|
+
catch { /* unknown backend name — keep the raw path */ }
|
|
5699
|
+
return workDir;
|
|
5700
|
+
}
|
|
5701
|
+
/**
|
|
5702
|
+
* Where each backend natively loads on-demand skills from, relative to the
|
|
5703
|
+
* workspace. Backends without a native skill mechanism (opencode, grok,
|
|
5704
|
+
* antigravity, gemini-cli) are deliberately absent: dropping files a CLI
|
|
5705
|
+
* never reads is clutter, not capability. Unknown directories are ignored
|
|
5706
|
+
* by older CLI versions, so publishing is fail-open across upgrades.
|
|
5707
|
+
*/
|
|
5708
|
+
static SKILLS_DIR_SEGMENTS = {
|
|
5709
|
+
"kiro-cli": [".kiro", "skills"],
|
|
5710
|
+
"claude-code": [".claude", "skills"],
|
|
5711
|
+
"codex": [".agents", "skills"],
|
|
5712
|
+
// Live-verified: OpenCode and Antigravity scan .agents/skills; Grok's
|
|
5713
|
+
// vendor-canonical location is .grok/skills.
|
|
5714
|
+
"opencode": [".agents", "skills"],
|
|
5715
|
+
"grok": [".grok", "skills"],
|
|
5716
|
+
"antigravity": [".agents", "skills"],
|
|
5717
|
+
};
|
|
5718
|
+
/** Copy general-knowledge steering + all role-eligible skills to General. */
|
|
5645
5719
|
syncGeneralKnowledge(workDir, backend) {
|
|
5646
5720
|
const knowledgeDir = join(dirname(fileURLToPath(import.meta.url)), "general-knowledge");
|
|
5647
5721
|
if (!existsSync(knowledgeDir))
|
|
5648
5722
|
return;
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5723
|
+
this.syncGeneralSteering(workDir, backend, join(knowledgeDir, "steering"));
|
|
5724
|
+
this.syncRoleSkills(workDir, backend, "general", knowledgeDir);
|
|
5725
|
+
this.logger.debug({ knowledgeDir, workDir, backend }, "Synced general knowledge files");
|
|
5726
|
+
}
|
|
5727
|
+
/** Publish only the bundled skills eligible for an instance role. */
|
|
5728
|
+
syncRoleSkills(workDir, backend, role, knowledgeDir) {
|
|
5729
|
+
const skillSegments = FleetManager.SKILLS_DIR_SEGMENTS[backend];
|
|
5730
|
+
if (!skillSegments)
|
|
5731
|
+
return;
|
|
5732
|
+
const root = knowledgeDir ?? join(dirname(fileURLToPath(import.meta.url)), "general-knowledge");
|
|
5733
|
+
if (!existsSync(root))
|
|
5734
|
+
return;
|
|
5735
|
+
// Before managed-skill manifests existed, only Kiro General received
|
|
5736
|
+
// bundled skills. Allow that one legacy layout to be adopted so upgrades
|
|
5737
|
+
// can keep those copies current; other backends never had unmanaged
|
|
5738
|
+
// AgEnD-published skills and must retain the normal user-ownership guard.
|
|
5739
|
+
const adoptLegacyUnmanaged = backend === "kiro-cli" && role === "general";
|
|
5740
|
+
this.syncManagedSkills(join(workDir, ...skillSegments), join(root, "skills"), role, adoptLegacyUnmanaged);
|
|
5741
|
+
this.logger.debug({ workDir, backend, role }, "Synced role-based bundled skills");
|
|
5742
|
+
}
|
|
5743
|
+
/**
|
|
5744
|
+
* Steering (always-on rules like core-rules.md). Kiro loads a native
|
|
5745
|
+
* steering directory; every other backend gets the content embedded into
|
|
5746
|
+
* its instructions file (CLAUDE.md / AGENTS.md / …) inside a managed marker
|
|
5747
|
+
* block — the previous behavior dropped bare .md files in the workspace
|
|
5748
|
+
* root, which no CLI ever read. The block is replaced in place on every
|
|
5749
|
+
* sync, so rule updates reach EXISTING workspaces; everything the user
|
|
5750
|
+
* wrote outside the markers is preserved byte-for-byte.
|
|
5751
|
+
*/
|
|
5752
|
+
syncGeneralSteering(workDir, backend, srcSteering) {
|
|
5753
|
+
if (!existsSync(srcSteering))
|
|
5754
|
+
return;
|
|
5755
|
+
const files = readdirSync(srcSteering).filter(f => f.endsWith(".md")).sort();
|
|
5756
|
+
if (files.length === 0)
|
|
5757
|
+
return;
|
|
5758
|
+
if (backend === "kiro-cli") {
|
|
5759
|
+
const steeringDir = join(workDir, ".kiro", "steering");
|
|
5760
|
+
mkdirSync(steeringDir, { recursive: true });
|
|
5761
|
+
for (const file of files) {
|
|
5660
5762
|
const dest = join(steeringDir, file);
|
|
5661
|
-
const newContent = readFileSync(
|
|
5763
|
+
const newContent = readFileSync(join(srcSteering, file), "utf-8");
|
|
5662
5764
|
try {
|
|
5663
5765
|
if (existsSync(dest) && readFileSync(dest, "utf-8") === newContent)
|
|
5664
5766
|
continue;
|
|
5665
5767
|
}
|
|
5666
|
-
catch { }
|
|
5768
|
+
catch { /* rewrite */ }
|
|
5667
5769
|
writeFileSync(dest, newContent);
|
|
5668
5770
|
}
|
|
5771
|
+
return;
|
|
5669
5772
|
}
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5773
|
+
const filename = FleetManager.INSTRUCTIONS_FILENAME[backend] ?? "CLAUDE.md";
|
|
5774
|
+
const instructionsPath = join(workDir, filename);
|
|
5775
|
+
const body = files.map(f => readFileSync(join(srcSteering, f), "utf-8").trim()).join("\n\n");
|
|
5776
|
+
const block = `${FleetManager.STEERING_BLOCK_BEGIN}\n${body}\n${FleetManager.STEERING_BLOCK_END}`;
|
|
5777
|
+
let existing = "";
|
|
5778
|
+
try {
|
|
5779
|
+
existing = existsSync(instructionsPath) ? readFileSync(instructionsPath, "utf-8") : "";
|
|
5780
|
+
}
|
|
5781
|
+
catch { /* treat as empty */ }
|
|
5782
|
+
const beginAt = existing.indexOf(FleetManager.STEERING_BLOCK_BEGIN);
|
|
5783
|
+
const endAt = existing.indexOf(FleetManager.STEERING_BLOCK_END);
|
|
5784
|
+
let next;
|
|
5785
|
+
if (beginAt !== -1 && endAt !== -1 && endAt > beginAt) {
|
|
5786
|
+
next = existing.slice(0, beginAt) + block + existing.slice(endAt + FleetManager.STEERING_BLOCK_END.length);
|
|
5787
|
+
}
|
|
5788
|
+
else {
|
|
5789
|
+
next = existing.trimEnd() + (existing.trim() ? "\n\n" : "") + block + "\n";
|
|
5790
|
+
}
|
|
5791
|
+
if (next !== existing) {
|
|
5792
|
+
mkdirSync(dirname(instructionsPath), { recursive: true });
|
|
5793
|
+
writeFileSync(instructionsPath, next);
|
|
5794
|
+
}
|
|
5795
|
+
}
|
|
5796
|
+
static STEERING_BLOCK_BEGIN = "<!-- >>> agend:core-rules — managed by AgEnD; edits inside this block are overwritten -->";
|
|
5797
|
+
static STEERING_BLOCK_END = "<!-- <<< agend:core-rules -->";
|
|
5798
|
+
/**
|
|
5799
|
+
* Publish AgEnD's bundled skills into a CLI's native skills directory,
|
|
5800
|
+
* owning ONLY what we published. A manifest records which skill names AgEnD
|
|
5801
|
+
* wrote; a bundled rename/removal deletes the stale managed copy, while a
|
|
5802
|
+
* skill the user created by hand is never listed and therefore never
|
|
5803
|
+
* touched — even if a future bundle happens to reuse its name (the sync
|
|
5804
|
+
* then skips it and logs, rather than overwrite the user's work). The sole
|
|
5805
|
+
* migration exception is the pre-manifest Kiro General layout explicitly
|
|
5806
|
+
* selected by adoptLegacyUnmanaged.
|
|
5807
|
+
*/
|
|
5808
|
+
syncManagedSkills(destSkills, srcSkills, role, adoptLegacyUnmanaged = false) {
|
|
5809
|
+
if (!existsSync(srcSkills))
|
|
5810
|
+
return;
|
|
5811
|
+
mkdirSync(destSkills, { recursive: true });
|
|
5812
|
+
const manifestPath = join(destSkills, ".agend-managed-skills.json");
|
|
5813
|
+
const hadManifest = existsSync(manifestPath);
|
|
5814
|
+
let previouslyManaged = [];
|
|
5815
|
+
try {
|
|
5816
|
+
const parsed = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
5817
|
+
if (Array.isArray(parsed))
|
|
5818
|
+
previouslyManaged = parsed.filter(n => typeof n === "string");
|
|
5819
|
+
}
|
|
5820
|
+
catch { /* first sync, or corrupt manifest — treat as owning nothing */ }
|
|
5821
|
+
const bundled = readdirSync(srcSkills)
|
|
5822
|
+
.filter(name => existsSync(join(srcSkills, name, "SKILL.md")))
|
|
5823
|
+
.sort();
|
|
5824
|
+
// Pre-manifest Kiro General copied bundled skills directly into
|
|
5825
|
+
// .kiro/skills. If every existing skill is still a bundled name, this is
|
|
5826
|
+
// the unambiguous legacy layout: adopt it once, update it below, and write
|
|
5827
|
+
// the ownership manifest. Any extra skill name keeps the directory fully
|
|
5828
|
+
// on the user-owned/collision path.
|
|
5829
|
+
if (!hadManifest && adoptLegacyUnmanaged) {
|
|
5830
|
+
const existing = readdirSync(destSkills, { withFileTypes: true })
|
|
5831
|
+
.filter(entry => entry.isDirectory() && existsSync(join(destSkills, entry.name, "SKILL.md")))
|
|
5832
|
+
.map(entry => entry.name)
|
|
5833
|
+
.sort();
|
|
5834
|
+
if (existing.length > 0 && existing.every(name => bundled.includes(name))) {
|
|
5835
|
+
previouslyManaged = existing;
|
|
5836
|
+
this.logger.info({ skills: existing, destSkills }, "Adopted legacy AgEnD skills into managed ownership");
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5839
|
+
const eligible = bundled.filter(name => {
|
|
5840
|
+
const roles = this.readManagedSkillRoles(join(srcSkills, name, "SKILL.md"));
|
|
5841
|
+
// General is the coordinator and receives both coordinator and worker
|
|
5842
|
+
// playbooks. Workers receive only skills explicitly marked for workers.
|
|
5843
|
+
return role === "general" || roles.includes("worker");
|
|
5844
|
+
});
|
|
5845
|
+
const managed = [];
|
|
5846
|
+
for (const name of eligible) {
|
|
5847
|
+
const destDir = join(destSkills, name);
|
|
5848
|
+
const dest = join(destDir, "SKILL.md");
|
|
5849
|
+
const isOurs = previouslyManaged.includes(name) || !existsSync(destDir);
|
|
5850
|
+
if (!isOurs) {
|
|
5851
|
+
// Name collision with a user-authored skill: theirs wins, loudly.
|
|
5852
|
+
this.logger.warn({ skill: name, destSkills }, "Skipping bundled skill — a skill of this name exists but was not published by AgEnD");
|
|
5853
|
+
continue;
|
|
5854
|
+
}
|
|
5855
|
+
managed.push(name);
|
|
5856
|
+
const newContent = readFileSync(join(srcSkills, name, "SKILL.md"), "utf-8");
|
|
5857
|
+
try {
|
|
5858
|
+
if (existsSync(dest) && readFileSync(dest, "utf-8") === newContent)
|
|
5859
|
+
continue;
|
|
5860
|
+
}
|
|
5861
|
+
catch { /* rewrite */ }
|
|
5862
|
+
mkdirSync(destDir, { recursive: true });
|
|
5863
|
+
writeFileSync(dest, newContent);
|
|
5864
|
+
}
|
|
5865
|
+
// Remove managed skills that are no longer bundled OR no longer eligible
|
|
5866
|
+
// for this role. This makes a shared → general-only metadata change take
|
|
5867
|
+
// effect on the next worker startup instead of leaving stale capability.
|
|
5868
|
+
for (const stale of previouslyManaged) {
|
|
5869
|
+
if (eligible.includes(stale))
|
|
5870
|
+
continue;
|
|
5871
|
+
try {
|
|
5872
|
+
rmSync(join(destSkills, stale), { recursive: true, force: true });
|
|
5873
|
+
this.logger.info({ skill: stale, destSkills }, "Removed retired AgEnD-managed skill");
|
|
5874
|
+
}
|
|
5875
|
+
catch (err) {
|
|
5876
|
+
this.logger.debug({ err, skill: stale }, "Failed to remove retired managed skill");
|
|
5692
5877
|
}
|
|
5693
5878
|
}
|
|
5694
|
-
|
|
5879
|
+
try {
|
|
5880
|
+
writeFileSync(manifestPath, JSON.stringify(managed, null, 2) + "\n");
|
|
5881
|
+
}
|
|
5882
|
+
catch (err) {
|
|
5883
|
+
this.logger.debug({ err, manifestPath }, "Failed to write managed-skills manifest");
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
/** Read AgEnD's roles extension from SKILL.md YAML frontmatter. */
|
|
5887
|
+
readManagedSkillRoles(skillPath) {
|
|
5888
|
+
const fallback = ["general"];
|
|
5889
|
+
try {
|
|
5890
|
+
const content = readFileSync(skillPath, "utf-8");
|
|
5891
|
+
const match = content.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
5892
|
+
if (!match)
|
|
5893
|
+
return fallback;
|
|
5894
|
+
const document = parseDocument(match[1]);
|
|
5895
|
+
if (document.errors.length > 0)
|
|
5896
|
+
throw document.errors[0];
|
|
5897
|
+
const frontmatter = document.toJS();
|
|
5898
|
+
if (!frontmatter || frontmatter.roles === undefined)
|
|
5899
|
+
return fallback;
|
|
5900
|
+
if (!Array.isArray(frontmatter.roles))
|
|
5901
|
+
throw new Error("roles must be an array");
|
|
5902
|
+
const roles = [...new Set(frontmatter.roles)]
|
|
5903
|
+
.filter((value) => value === "general" || value === "worker");
|
|
5904
|
+
if (roles.length !== frontmatter.roles.length || roles.length === 0) {
|
|
5905
|
+
throw new Error("roles must contain only general and/or worker");
|
|
5906
|
+
}
|
|
5907
|
+
return roles;
|
|
5908
|
+
}
|
|
5909
|
+
catch (err) {
|
|
5910
|
+
// Fail closed for workers: malformed or unknown metadata keeps the
|
|
5911
|
+
// backwards-compatible General-only behavior instead of leaking an
|
|
5912
|
+
// administrative skill into worker workspaces.
|
|
5913
|
+
this.logger.warn({ err, skillPath }, "Invalid bundled skill roles — defaulting to General only");
|
|
5914
|
+
return fallback;
|
|
5915
|
+
}
|
|
5695
5916
|
}
|
|
5696
5917
|
/** Fetch forum topic icon stickers and pick emoji IDs for each state */
|
|
5697
5918
|
async resolveTopicIcons() {
|