@drunkcoding/agents-and-skills 0.0.43 → 0.0.45

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.
@@ -12,7 +12,7 @@
12
12
  "name": "tech-graph",
13
13
  "source": "./plugins/tech-graph",
14
14
  "description": "6-step wizard for technical diagrams (SVG/PNG) via fireworks-tech-graph",
15
- "version": "0.0.43",
15
+ "version": "0.0.45",
16
16
  "category": "diagram",
17
17
  "keywords": [
18
18
  "diagram",
@@ -26,7 +26,7 @@
26
26
  "name": "html-effectiveness",
27
27
  "source": "./plugins/html-effectiveness",
28
28
  "description": "Generate self-contained interactive HTML reports from 20 upstream templates via a conversational agent.",
29
- "version": "0.0.43",
29
+ "version": "0.0.45",
30
30
  "category": "reports",
31
31
  "keywords": [
32
32
  "html",
@@ -41,7 +41,7 @@
41
41
  "name": "plugin-validator",
42
42
  "source": "./plugins/plugin-validator",
43
43
  "description": "Orchestrated validator for Claude Code plugins — validates skills, agents, commands, and hooks across every plugin under plugins/**.",
44
- "version": "0.0.43",
44
+ "version": "0.0.45",
45
45
  "category": "tooling",
46
46
  "keywords": [
47
47
  "validation",
@@ -57,7 +57,7 @@
57
57
  "name": "team-share",
58
58
  "source": "./plugins/team-share",
59
59
  "description": "Onboard your team with an interactive setup menu: install CodeGraph, build the Understand-Anything knowledge graph, and share Claude Code settings — run any combination, all idempotent.",
60
- "version": "0.0.43",
60
+ "version": "0.0.45",
61
61
  "category": "tooling",
62
62
  "keywords": [
63
63
  "onboarding",
@@ -73,7 +73,7 @@
73
73
  "name": "multica-tool",
74
74
  "source": "./plugins/multica-tool",
75
75
  "description": "Export, import, and sync Multica skills, agents, and squads between workspaces.",
76
- "version": "0.0.43",
76
+ "version": "0.0.45",
77
77
  "category": "workflow",
78
78
  "keywords": [
79
79
  "multica",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drunkcoding/agents-and-skills",
3
- "version": "0.0.43",
3
+ "version": "0.0.45",
4
4
  "description": "Personal collection of Claude Code skills and agents, installable via `npx skills`.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "html-effectiveness",
3
3
  "displayName": "HTML Effectiveness Reports",
4
- "version": "0.0.43",
4
+ "version": "0.0.45",
5
5
  "description": "Generate self-contained interactive HTML reports from 20 upstream templates via a conversational agent.",
6
6
  "author": {
7
7
  "name": "Steven Hoang"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "multica-tool",
3
- "version": "0.0.43",
3
+ "version": "0.0.45",
4
4
  "description": "Export, import, and sync Multica skills, agents, and squads between workspaces via the multica CLI."
5
5
  }
@@ -53,6 +53,9 @@ export function resolveWorkspaceId(cli, name) {
53
53
  export const listRuntimes = (cli) => cli.json(["runtime", "list"]);
54
54
  export const listSkills = (cli) => cli.json(["skill", "list"]);
55
55
  export const listAgents = (cli) => cli.json(["agent", "list"]);
56
+ // Includes archived agents (archived_at set) — used wherever a name match or an
57
+ // id lookup must also see agents excluded from the default listing above.
58
+ export const listAgentsIncludingArchived = (cli) => cli.json(["agent", "list", "--include-archived"]);
56
59
  export const listSquads = (cli) => cli.json(["squad", "list"]);
57
60
  export const listWorkspaceMembers = (cli) => cli.json(["workspace", "member", "list"]);
58
61
 
@@ -72,6 +75,8 @@ export function getAgent(cli, id) {
72
75
  const a = cli.json(["agent", "get", id]);
73
76
  return {
74
77
  id: a.id, name: a.name, description: a.description, instructions: a.instructions,
78
+ // Export-time signal only (never written to a bundle record — see redactAgent).
79
+ archived_at: a.archived_at ?? null,
75
80
  model: a.model, visibility: a.visibility, avatar_url: a.avatar_url ?? null,
76
81
  service_tier: a.service_tier ?? "",
77
82
  permission_mode: a.permission_mode ?? null,
@@ -1,10 +1,22 @@
1
1
  import * as nodeFs from "node:fs";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { dirname } from "node:path";
4
- import { slugify, getSkill, getAgent, getAgentCustomEnv, getSquad, getSquadMembers, listRuntimes, listSkills, listAgents, listSquads, listProjects, getProject, getProjectResources, listWorkspaceMembers, getAutopilot, makeCli, realExec, requireAuth, resolveWorkspaceId } from "./lib.mjs";
4
+ import { slugify, getSkill, getAgent, getAgentCustomEnv, getSquad, getSquadMembers, listRuntimes, listSkills, listAgents, listAgentsIncludingArchived, listSquads, listProjects, getProject, getProjectResources, listWorkspaceMembers, getAutopilot, makeCli, realExec, requireAuth, resolveWorkspaceId } from "./lib.mjs";
5
5
 
6
6
  const nonEmpty = (v) => v && typeof v === "object" && Object.keys(v).length > 0;
7
7
 
8
+ // Externalize a prose field to a sibling .md next to `jsonRel`
9
+ // (e.g. agents/x.json + ".description.md" → agents/x.description.md), and record
10
+ // the `<field>_file` pointer on `record`. Empty prose writes nothing and adds no
11
+ // key — same sibling-file pattern as avatar_file, applied uniformly to every
12
+ // resource's instructions/description so no prose is ever embedded in the JSON.
13
+ function writeSidecar(fs, outDir, jsonRel, suffix, text, record, key) {
14
+ if (!text) return;
15
+ const rel = jsonRel.replace(/\.json$/, suffix);
16
+ fs.writeFileSync(`${outDir}/${rel}`, text);
17
+ record[key] = rel;
18
+ }
19
+
8
20
  // An avatar_url is either an uploaded-image URL (http[s]://…) or an inline
9
21
  // "emoji:🦍" marker. Only image URLs carry bytes worth bundling; emoji markers
10
22
  // are just carried as strings in the record.
@@ -31,7 +43,7 @@ export function redactAgent(a) {
31
43
  // a is a normalized agent from getAgent, with `custom_env`/
32
44
  // `custom_env_fetch_failed` attached by the caller (collectAgent) — getAgent
33
45
  // itself never fetches custom_env, since it requires a separate audited call.
34
- const { id, has_custom_env, mcp_config_redacted, custom_env_fetch_failed, mcp_config, custom_env, skills, runtime_id, instructions, ...rest } = a;
46
+ const { id, has_custom_env, mcp_config_redacted, custom_env_fetch_failed, mcp_config, custom_env, skills, runtime_id, instructions, description, archived_at, ...rest } = a;
35
47
  const mcpUsable = !mcp_config_redacted && nonEmpty(mcp_config);
36
48
  const envUsable = !custom_env_fetch_failed && nonEmpty(custom_env);
37
49
  // mcp_config_redacted / custom_env_fetch_failed alone still flag hadSecrets even
@@ -51,9 +63,10 @@ export function redactAgent(a) {
51
63
  had_secrets: hadSecrets,
52
64
  },
53
65
  hadSecrets,
54
- // instructions are written to a sibling .md by the caller (see avatar_file),
55
- // never embedded in the JSON record.
66
+ // instructions and description are written to sibling .md files by the caller
67
+ // (see avatar_file), never embedded in the JSON record.
56
68
  instructions: instructions ?? "",
69
+ description: description ?? "",
57
70
  };
58
71
  }
59
72
 
@@ -72,9 +85,11 @@ export function buildManifest({ scope, sourceWorkspaceId, skills, agents, squads
72
85
  agents: [...seenAgents.values()].map((a) => ({ name: a.name, file: `agents/${slugify(a.name)}.json`, source_id: a.source_id, source_runtime_id: a.source_runtime_id, source_runtime_provider: a.source_runtime_provider ?? null, skill_names: a.skill_names, had_secrets: !!a.had_secrets })),
73
86
  squads: (squads ?? []).map((squad) => {
74
87
  const file = `squads/${slugify(squad.name)}.json`;
75
- const entry = { name: squad.name, file, description: squad.description ?? "", avatar_url: squad.avatar_url ?? null, leader_name: squad.leader_name, members: squad.members };
76
- // Instructions go to a sibling .md (see squad write loop); only referenced when non-empty.
88
+ const entry = { name: squad.name, file, avatar_url: squad.avatar_url ?? null, leader_name: squad.leader_name, members: squad.members };
89
+ // Instructions and description go to sibling .md files (see squad write
90
+ // loop); only referenced when non-empty.
77
91
  if (squad.instructions) entry.instructions_file = file.replace(/\.json$/, ".md");
92
+ if (squad.description) entry.description_file = file.replace(/\.json$/, ".description.md");
78
93
  return entry;
79
94
  }),
80
95
  projects: [...seenProjects.values()].map((p) => ({
@@ -96,10 +111,15 @@ function collectSkill(cli, id, skills) {
96
111
  }
97
112
 
98
113
  // Keyed by agent id (so squad leader_id/member_id resolve to names). Stores the
99
- // normalized agent, its redaction result, and its skill names.
114
+ // normalized agent, its redaction result, and its skill names. Returns
115
+ // `{ archived: true, name }` instead of collecting when the agent is archived —
116
+ // callers reached via a squad/project/explicit id lookup must decide how to
117
+ // handle that (the workspace-listing loop never hits this: listAgents() already
118
+ // excludes archived agents server-side).
100
119
  function collectAgent(cli, id, agentsById, skills, providerById) {
101
120
  if (agentsById.has(id)) return agentsById.get(id);
102
121
  const a = getAgent(cli, id);
122
+ if (a.archived_at) return { archived: true, name: a.name };
103
123
  a.source_runtime_provider = providerById.get(a.runtime_id) ?? null;
104
124
  a.custom_env = {};
105
125
  a.custom_env_fetch_failed = false;
@@ -117,22 +137,6 @@ function collectAgent(cli, id, agentsById, skills, providerById) {
117
137
  return entry;
118
138
  }
119
139
 
120
- // Collect a project's portable metadata + its lead agent (bundled, like a squad
121
- // leader) and github_repo/other resources. Returns the project bundle record.
122
- function collectProject(cli, id, agentsById, skills, providerById) {
123
- const p = getProject(cli, id);
124
- let lead_name = null;
125
- if (p.lead_type === "agent" && p.lead_id) {
126
- lead_name = collectAgent(cli, p.lead_id, agentsById, skills, providerById).raw.name;
127
- }
128
- return {
129
- title: p.title, description: p.description, icon: p.icon,
130
- priority: p.priority, status: p.status, due_date: p.due_date, start_date: p.start_date,
131
- source_id: id, lead_type: p.lead_type, lead_name, lead_source_id: p.lead_id,
132
- resources: getProjectResources(cli, id),
133
- };
134
- }
135
-
136
140
  export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs = nodeFs, download = fetchBinary }) {
137
141
  const skills = new Map(); // name -> normalized skill
138
142
  const agentsById = new Map(); // id -> { raw, red, skill_names }
@@ -144,12 +148,34 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
144
148
  let providerById = null;
145
149
  const getProviderById = () => providerById ??= new Map(listRuntimes(cli).map((r) => [r.id, r.provider]));
146
150
 
147
- // Collect a squad's agents (leader + members) and return the squad bundle object.
151
+ // Archived agents skipped anywhere below, deduped by (name, path, detail)
152
+ // the operator-facing "why isn't X in my bundle" report.
153
+ const archivedAgentsSkipped = [];
154
+ const seenArchivedSkip = new Set();
155
+ function recordArchivedSkip(name, path, detail) {
156
+ const key = [name, path, detail ?? ""].join("|");
157
+ if (seenArchivedSkip.has(key)) return;
158
+ seenArchivedSkip.add(key);
159
+ archivedAgentsSkipped.push(detail ? { name, path, detail } : { name, path });
160
+ }
161
+
162
+ // Collect a squad's agents (leader + members) and return the squad bundle
163
+ // object — or null when the leader is archived, since a squad can't import
164
+ // without one (the whole squad is excluded from the bundle, not emitted leaderless).
148
165
  function collectOneSquad(squadId) {
149
166
  const sq = getSquad(cli, squadId);
150
- const members = getSquadMembers(cli, squadId).filter((m) => m.member_type === "agent");
151
- for (const m of members) collectAgent(cli, m.member_id, agentsById, skills, getProviderById());
152
- if (!agentsById.has(sq.leader_id)) collectAgent(cli, sq.leader_id, agentsById, skills, getProviderById());
167
+ const leaderEntry = collectAgent(cli, sq.leader_id, agentsById, skills, getProviderById());
168
+ if (leaderEntry.archived) {
169
+ recordArchivedSkip(leaderEntry.name, "squad leader", sq.name);
170
+ return null;
171
+ }
172
+ const allMembers = getSquadMembers(cli, squadId).filter((m) => m.member_type === "agent");
173
+ const members = [];
174
+ for (const m of allMembers) {
175
+ const entry = collectAgent(cli, m.member_id, agentsById, skills, getProviderById());
176
+ if (entry.archived) { recordArchivedSkip(entry.name, "squad member", sq.name); continue; }
177
+ members.push(m);
178
+ }
153
179
  const nameOf = (id) => agentsById.get(id)?.raw.name;
154
180
  return {
155
181
  name: sq.name, description: sq.description, instructions: sq.instructions, avatar_url: sq.avatar_url,
@@ -158,6 +184,30 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
158
184
  };
159
185
  }
160
186
 
187
+ // Collect a project's portable metadata + its lead agent (bundled, like a
188
+ // squad leader) and github_repo/other resources. An archived lead is dropped
189
+ // (project still exports, just with no lead set) — unlike a squad, a project
190
+ // has no hard requirement for one.
191
+ function collectProject(id) {
192
+ const p = getProject(cli, id);
193
+ let lead_name = null, lead_type = p.lead_type, lead_source_id = p.lead_id;
194
+ if (p.lead_type === "agent" && p.lead_id) {
195
+ const entry = collectAgent(cli, p.lead_id, agentsById, skills, getProviderById());
196
+ if (entry.archived) {
197
+ recordArchivedSkip(entry.name, "project lead", p.title);
198
+ lead_type = null; lead_source_id = null;
199
+ } else {
200
+ lead_name = entry.raw.name;
201
+ }
202
+ }
203
+ return {
204
+ title: p.title, description: p.description, icon: p.icon,
205
+ priority: p.priority, status: p.status, due_date: p.due_date, start_date: p.start_date,
206
+ source_id: id, lead_type, lead_name, lead_source_id,
207
+ resources: getProjectResources(cli, id),
208
+ };
209
+ }
210
+
161
211
  // Collect a single autopilot: its assignee (agent, or squad + members + skills)
162
212
  // and, when present, the destination-portable project title / subscriber names.
163
213
  // Webhook trigger secrets (url/token) are never read into the bundle — only
@@ -166,11 +216,11 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
166
216
  const ap = getAutopilot(cli, autopilotId);
167
217
  let assignee_name = null;
168
218
  if (ap.assignee_type === "agent") {
169
- assignee_name = collectAgent(cli, ap.assignee_id, agentsById, skills, getProviderById()).raw.name;
219
+ const entry = collectAgent(cli, ap.assignee_id, agentsById, skills, getProviderById());
220
+ assignee_name = entry.archived ? null : entry.raw.name;
170
221
  } else if (ap.assignee_type === "squad") {
171
222
  const sq = collectOneSquad(ap.assignee_id);
172
- squads.push(sq);
173
- assignee_name = sq.name;
223
+ if (sq) { squads.push(sq); assignee_name = sq.name; }
174
224
  }
175
225
  let project_title = null;
176
226
  if (ap.project_id) {
@@ -193,16 +243,24 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
193
243
  }
194
244
 
195
245
  if (scope === "skill") collectSkill(cli, ids.skillId, skills);
196
- else if (scope === "agent") collectAgent(cli, ids.agentId, agentsById, skills, getProviderById());
197
- else if (scope === "squad") squads.push(collectOneSquad(ids.squadId));
198
- else if (scope === "project") projects.push(collectProject(cli, ids.projectId, agentsById, skills, getProviderById()));
199
- else if (scope === "projects") for (const p of listProjects(cli)) projects.push(collectProject(cli, p.id, agentsById, skills, getProviderById()));
246
+ else if (scope === "agent") {
247
+ const entry = collectAgent(cli, ids.agentId, agentsById, skills, getProviderById());
248
+ if (entry.archived) throw new Error(`Cannot export agent "${entry.name}": it is archived`);
249
+ }
250
+ else if (scope === "squad") { const sq = collectOneSquad(ids.squadId); if (sq) squads.push(sq); }
251
+ else if (scope === "project") projects.push(collectProject(ids.projectId));
252
+ else if (scope === "projects") for (const p of listProjects(cli)) projects.push(collectProject(p.id));
200
253
  else if (scope === "autopilot") autopilots.push(collectOneAutopilot(ids.autopilotId));
201
254
  else if (scope === "all") {
202
255
  for (const s of listSkills(cli)) collectSkill(cli, s.id, skills);
203
256
  for (const a of listAgents(cli)) collectAgent(cli, a.id, agentsById, skills, getProviderById());
204
- for (const sq of listSquads(cli)) squads.push(collectOneSquad(sq.id));
205
- for (const p of listProjects(cli)) projects.push(collectProject(cli, p.id, agentsById, skills, getProviderById()));
257
+ for (const sq of listSquads(cli)) { const built = collectOneSquad(sq.id); if (built) squads.push(built); }
258
+ for (const p of listProjects(cli)) projects.push(collectProject(p.id));
259
+ // listAgents() above already excludes archived agents server-side — surface
260
+ // that exclusion in the report too, not just the squad/project paths.
261
+ for (const a of listAgentsIncludingArchived(cli)) {
262
+ if (a.archived_at) recordArchivedSkip(a.name, "workspace listing");
263
+ }
206
264
  }
207
265
 
208
266
  // Orphan-skill cleanup: drop skills that no exported agent references via its
@@ -261,36 +319,38 @@ export function exportResource({ cli, scope, ids, outDir, sourceWorkspaceId, fs
261
319
  record.avatar_file = rel;
262
320
  }
263
321
  }
264
- // Instructions live in a sibling .md for reviewability (same sibling-file
265
- // pattern as avatar_file); only written when non-empty.
266
- if (red.instructions) {
267
- const rel = entry.file.replace(/\.json$/, ".md");
268
- fs.writeFileSync(`${outDir}/${rel}`, red.instructions);
269
- record.instructions_file = rel;
270
- }
322
+ // Instructions and description each live in a sibling .md for reviewability.
323
+ writeSidecar(fs, outDir, entry.file, ".md", red.instructions, record, "instructions_file");
324
+ writeSidecar(fs, outDir, entry.file, ".description.md", red.description, record, "description_file");
271
325
  fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(record, null, 2));
272
326
  }
273
- const squadInstrByName = new Map(squads.map((s) => [s.name, s.instructions ?? ""]));
327
+ const squadByName = new Map(squads.map((s) => [s.name, s]));
274
328
  for (const entry of manifest.squads) {
275
329
  fs.mkdirSync(`${outDir}/squads`, { recursive: true });
276
- if (entry.instructions_file) {
277
- fs.writeFileSync(`${outDir}/${entry.instructions_file}`, squadInstrByName.get(entry.name) ?? "");
278
- }
330
+ const s = squadByName.get(entry.name);
331
+ if (entry.instructions_file) fs.writeFileSync(`${outDir}/${entry.instructions_file}`, s?.instructions ?? "");
332
+ if (entry.description_file) fs.writeFileSync(`${outDir}/${entry.description_file}`, s?.description ?? "");
279
333
  fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(entry, null, 2));
280
334
  }
281
335
  const projectByTitle = new Map(projects.map((p) => [p.title, p]));
282
336
  for (const entry of manifest.projects) {
283
337
  fs.mkdirSync(`${outDir}/projects`, { recursive: true });
284
- fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(projectByTitle.get(entry.title), null, 2));
338
+ const record = { ...projectByTitle.get(entry.title) };
339
+ const desc = record.description; delete record.description;
340
+ writeSidecar(fs, outDir, entry.file, ".description.md", desc, record, "description_file");
341
+ fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(record, null, 2));
285
342
  }
286
343
  const autopilotByTitle = new Map(autopilots.map((a) => [a.title, a]));
287
344
  for (const entry of manifest.autopilots) {
288
345
  fs.mkdirSync(`${outDir}/autopilots`, { recursive: true });
289
- fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(autopilotByTitle.get(entry.title), null, 2));
346
+ const record = { ...autopilotByTitle.get(entry.title) };
347
+ const desc = record.description; delete record.description;
348
+ writeSidecar(fs, outDir, entry.file, ".description.md", desc, record, "description_file");
349
+ fs.writeFileSync(`${outDir}/${entry.file}`, JSON.stringify(record, null, 2));
290
350
  }
291
351
  fs.writeFileSync(`${outDir}/manifest.json`, JSON.stringify(manifest, null, 2));
292
352
  return {
293
- manifest, warnings, pruned_skills,
353
+ manifest, warnings, pruned_skills, archivedAgentsSkipped,
294
354
  autopilotWebhookTriggers: autopilots.filter((a) => a.had_webhook_trigger).map((a) => a.title),
295
355
  };
296
356
  }
@@ -1,5 +1,5 @@
1
1
  import * as nodeFs from "node:fs";
2
- import { listSkills, listAgents, listSquads, listRuntimes, listWorkspaceMembers, getSquadMembers, findByName, makeCli, realExec, requireAuth, resolveWorkspaceId, listProjects, getProjectResources, findByTitle, listAutopilots, getAutopilot } from "./lib.mjs";
2
+ import { listSkills, listAgents, listAgentsIncludingArchived, listSquads, listRuntimes, listWorkspaceMembers, getSquadMembers, findByName, makeCli, realExec, requireAuth, resolveWorkspaceId, listProjects, getProjectResources, findByTitle, listAutopilots, getAutopilot } from "./lib.mjs";
3
3
 
4
4
  // User-facing selectable types are agents/squads/projects/autopilots; skills follow agents.
5
5
  export function parseInclude(raw) {
@@ -8,15 +8,18 @@ export function parseInclude(raw) {
8
8
  return set;
9
9
  }
10
10
 
11
- // Instructions live in a sibling .md referenced by `instructions_file` (mirrors
12
- // avatar_file). Legacy bundles carry no instructions_file and keep instructions
13
- // inline in the JSON — fall back to that so older exports still import.
14
- function readInstructions(fs, dir, rec) {
15
- if (rec.instructions_file && fs.existsSync(`${dir}/${rec.instructions_file}`)) {
16
- return fs.readFileSync(`${dir}/${rec.instructions_file}`, "utf8");
11
+ // A prose field (instructions/description) lives in a sibling .md referenced by
12
+ // `<field>_file` (mirrors avatar_file). Legacy bundles carry no `<field>_file`
13
+ // and keep the prose inline in the JSON — fall back to that so older exports
14
+ // still import.
15
+ function readSidecar(fs, dir, rec, fileKey, inlineKey) {
16
+ if (rec[fileKey] && fs.existsSync(`${dir}/${rec[fileKey]}`)) {
17
+ return fs.readFileSync(`${dir}/${rec[fileKey]}`, "utf8");
17
18
  }
18
- return rec.instructions ?? "";
19
+ return rec[inlineKey] ?? "";
19
20
  }
21
+ const readInstructions = (fs, dir, rec) => readSidecar(fs, dir, rec, "instructions_file", "instructions");
22
+ const readDescription = (fs, dir, rec) => readSidecar(fs, dir, rec, "description_file", "description");
20
23
 
21
24
  // Relative paths of every file under root (recursing into subdirs like scripts/).
22
25
  function walkSkillFiles(fs, root, rel = "") {
@@ -86,8 +89,10 @@ export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs =
86
89
  // Lazy + memoized: destination member user_ids, only listed when an agent needs them.
87
90
  let destMemberIds = null;
88
91
  const getDestMemberIds = () => destMemberIds ??= new Set(listWorkspaceMembers(cli).map((m) => m.user_id));
89
- let created = 0, updated = 0;
90
- const existing = listAgents(cli);
92
+ let created = 0, updated = 0, reused = 0;
93
+ // Includes archived agents so a bundle name matching a retired agent restores
94
+ // and reuses it instead of failing to create under the held name.
95
+ const existing = listAgentsIncludingArchived(cli);
91
96
 
92
97
  for (const a of manifest.agents) {
93
98
  const rec = JSON.parse(fs.readFileSync(`${dir}/${a.file}`, "utf8"));
@@ -98,7 +103,8 @@ export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs =
98
103
  "--visibility", rec.visibility ?? "private",
99
104
  "--max-concurrent-tasks", String(rec.max_concurrent_tasks ?? 6),
100
105
  ];
101
- if (rec.description) common.push("--description", rec.description);
106
+ const description = readDescription(fs, dir, rec);
107
+ if (description) common.push("--description", description);
102
108
  const instructions = readInstructions(fs, dir, rec);
103
109
  if (instructions) common.push("--instructions", instructions);
104
110
  if (rec.model) common.push("--model", rec.model);
@@ -108,7 +114,14 @@ export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs =
108
114
  if (rec.service_tier) common.push("--service-tier", rec.service_tier);
109
115
  const match = findByName(existing, rec.name);
110
116
  let id;
111
- if (match) {
117
+ if (match && match.archived_at) {
118
+ // Restored agent then updated exactly like an active-name match — the
119
+ // matched id is now active, so a re-import of this bundle will find it
120
+ // via the plain `match` branch below and never restore twice.
121
+ cli.run(["agent", "restore", match.id]);
122
+ cli.run(["agent", "update", match.id, "--runtime-id", targetRuntime, ...common]);
123
+ id = match.id; reused++;
124
+ } else if (match) {
112
125
  cli.run(["agent", "update", match.id, "--runtime-id", targetRuntime, ...common]);
113
126
  id = match.id; updated++;
114
127
  } else {
@@ -178,7 +191,7 @@ export function importAgents({ cli, manifest, dir, skillIdMap, runtimeMap, fs =
178
191
  }
179
192
  }
180
193
  }
181
- return { idMap, sourceIdMap, created, updated, secretsApplyFailures, avatarApplyFailures, avatarUnsupported, permissionApplyFailures, permissionUnsupported };
194
+ return { idMap, sourceIdMap, created, updated, reused, secretsApplyFailures, avatarApplyFailures, avatarUnsupported, permissionApplyFailures, permissionUnsupported };
182
195
  }
183
196
 
184
197
  // Rewrites `mention://agent/<id>` links (e.g. `[@dev-backend](mention://agent/<id>)`)
@@ -260,7 +273,8 @@ export function importProjects({ cli, manifest, dir, agentIdMap, fs = nodeFs })
260
273
  for (const entry of manifest.projects ?? []) {
261
274
  const rec = JSON.parse(fs.readFileSync(`${dir}/${entry.file}`, "utf8"));
262
275
  const flags = ["--title", rec.title];
263
- if (rec.description) flags.push("--description", rec.description);
276
+ const description = readDescription(fs, dir, rec);
277
+ if (description) flags.push("--description", description);
264
278
  if (rec.icon) flags.push("--icon", rec.icon);
265
279
  if (rec.status) flags.push("--status", rec.status);
266
280
  if (rec.due_date) flags.push("--due-date", rec.due_date);
@@ -338,7 +352,8 @@ export function importAutopilots({ cli, manifest, dir, agentIdMap, fs = nodeFs }
338
352
 
339
353
  for (const rec of recs) {
340
354
  const common = [];
341
- if (rec.description) common.push("--description", rec.description);
355
+ const description = readDescription(fs, dir, rec);
356
+ if (description) common.push("--description", description);
342
357
  if (rec.issue_title_template) common.push("--issue-title-template", rec.issue_title_template);
343
358
  // priority is never present today — the multica CLI/API accepts it on write but
344
359
  // never returns it on read, so export can't capture the source's real value.
@@ -448,7 +463,7 @@ export function importBundle({ cli, dir, runtimeMap, include, fs = nodeFs }) {
448
463
  : { idMap: new Map(), created: 0, updated: 0 };
449
464
  const agentRes = inc.has("agents")
450
465
  ? importAgents({ cli, manifest, dir, skillIdMap: skillRes.idMap, runtimeMap: effective, fs })
451
- : { idMap: new Map(), sourceIdMap: new Map(), created: 0, updated: 0, secretsApplyFailures: [], avatarApplyFailures: [], avatarUnsupported: [], permissionApplyFailures: [], permissionUnsupported: [] };
466
+ : { idMap: new Map(), sourceIdMap: new Map(), created: 0, updated: 0, reused: 0, secretsApplyFailures: [], avatarApplyFailures: [], avatarUnsupported: [], permissionApplyFailures: [], permissionUnsupported: [] };
452
467
  // Runs after every agent exists so forward-referencing mentions resolve.
453
468
  const mentionRes = inc.has("agents")
454
469
  ? rewriteAgentMentions({ cli, manifest, dir, agentIdMap: agentRes.idMap, sourceIdMap: agentRes.sourceIdMap, fs })
@@ -460,6 +475,7 @@ export function importBundle({ cli, dir, runtimeMap, include, fs = nodeFs }) {
460
475
  if (inc.has("squads")) {
461
476
  for (const squad of manifest.squads ?? []) {
462
477
  squad.instructions = readInstructions(fs, dir, squad);
478
+ squad.description = readDescription(fs, dir, squad);
463
479
  const r = importSquad({ cli, squad, agentIdMap: agentRes.idMap, sourceIdMap: agentRes.sourceIdMap });
464
480
  if (r.skipped) { squadsSkipped.push(squad.name); continue; }
465
481
  squadIdMap.set(squad.name, r.newId);
@@ -482,6 +498,9 @@ export function importBundle({ cli, dir, runtimeMap, include, fs = nodeFs }) {
482
498
  include: [...inc],
483
499
  created: { skills: skillRes.created, agents: agentRes.created, squads: squadsCreated, projects: projectRes.created, autopilots: autopilotRes.created },
484
500
  updated: { skills: skillRes.updated, agents: agentRes.updated, squads: squadsUpdated, projects: projectRes.updated, autopilots: autopilotRes.updated },
501
+ // Agents restored from archived + updated to the bundle's definition — distinct
502
+ // from a plain update (an active-name match). Only agents can be reused today.
503
+ reused: { agents: agentRes.reused },
485
504
  mentionsRewritten: mentionRes.updated,
486
505
  skillIdMap: Object.fromEntries(skillRes.idMap),
487
506
  agentIdMap: Object.fromEntries(agentRes.idMap),
@@ -531,6 +550,16 @@ export function preflight({ cli, dir, runtimeMap, include, fs = nodeFs }) {
531
550
  ? `${u.srcId} (provider "${u.provider}": ${u.matchCount} matches, expected 1)`
532
551
  : `${u.srcId} (no provider recorded)` });
533
552
  }
553
+
554
+ // Read-only check, mirrors importAgents' restore-and-reuse match — never
555
+ // calls restore itself, just reports what a real import would do.
556
+ const existingAgents = listAgentsIncludingArchived(cli);
557
+ for (const a of manifest.agents ?? []) {
558
+ const match = findByName(existingAgents, a.name);
559
+ if (match && match.archived_at) {
560
+ incompatibilities.push({ type: "agent-archived-will-restore", detail: `${a.name} (archived in destination — import would restore and reuse it)` });
561
+ }
562
+ }
534
563
  }
535
564
 
536
565
  if (inc.has("projects")) {
@@ -37,13 +37,7 @@ If the user specified an output directory, use it verbatim. Otherwise default to
37
37
  First resolve `<workspace-name>`:
38
38
 
39
39
  - If the user named a source workspace (the value you would pass as `--workspace <name>`), use that name.
40
- - Otherwise, read the current default workspace's name:
41
-
42
- ```bash
43
- multica workspace get --output json
44
- ```
45
-
46
- and take its `.name`.
40
+ - Otherwise, run `multica workspace get --output json` and take its `.name` (the current default workspace).
47
41
 
48
42
  Slugify the resolved name for filesystem safety — lowercase it, replace each run of non-`[a-z0-9]` characters with a single `-`, and trim leading/trailing `-` (the same rule the scripts use internally).
49
43
 
@@ -70,7 +64,12 @@ Pass `--scope all` (with no `--id`) to export the **entire workspace** — every
70
64
 
71
65
  Exporting a project (or `projects`/`all`) also **bundles the project's lead agent** so the bundle is self-contained; projects carry metadata only (title, description, icon, priority, status, dates, lead mapping) plus their attached resource records — never issues. On import, only `github_repo` resources are portable and recreated; other resource types are reported and skipped.
72
66
 
73
- The script writes `manifest.json`, skill `SKILL.md` files, agent JSON files, and squad JSON files into `<dir>`. Each agent's and squad's **instructions** (system prompt / charter) are written to a sibling Markdown file `agents/<slug>.md`, `squads/<slug>.md` — referenced by an `instructions_file` key in the JSON, so the prose is easy to read, diff, and edit. Agents/squads with no instructions get no `.md`.
67
+ The script writes `manifest.json`, skill `SKILL.md` files, agent JSON files, and squad JSON files into `<dir>`. Every resource's prose fields are externalized to sibling Markdown files, never embedded in the JSON so they are easy to read, diff, and edit:
68
+
69
+ - **instructions** (system prompt / charter, agents and squads) → `<slug>.md`, referenced by an `instructions_file` key.
70
+ - **description** (agents, squads, projects, autopilots) → `<slug>.description.md`, referenced by a `description_file` key.
71
+
72
+ An empty field gets no file and no `*_file` key. Skills keep their own layout — the description lives in `SKILL.md` frontmatter and the body is the content.
74
73
 
75
74
  Avatars are captured automatically: an agent's uploaded-image avatar is downloaded into the bundle (`agents/<slug>.avatar.<ext>`) and referenced by `avatar_file`; emoji avatars (agents and squads) and a squad's avatar are recorded as the `avatar_url` string.
76
75
 
@@ -13,7 +13,7 @@ Import a local Multica bundle (produced by the export skill) into a target works
13
13
  You need the exact workspace name as registered in Multica for `--workspace` in the steps below.
14
14
 
15
15
  - If the user named a target workspace, use it.
16
- - If the user did **not** name one, default to the **basename of the import folder** — e.g. importing from `export/mx-workspace` defaults the target workspace to `mx-workspace`. State the inferred workspace name to the user before continuing.
16
+ - If the user did **not** name one, default to the **basename of the import folder** — e.g. importing from `export/mx-workspace` defaults the target workspace to `mx-workspace`. State the inferred workspace name to the user before continuing. This default is reliable for whole-workspace bundles (`export/<workspace-name>`); a single-resource bundle nested under `export/<workspace-name>/<slug>-<type>` has the resource slug as its basename, not the workspace — name the target workspace explicitly in that case.
17
17
 
18
18
  No existence check is needed here: the Step 2 dry-run fails with `Unknown workspace "<name>"` if the inferred workspace is not present in the target account, at which point ask the user for the correct name.
19
19
 
@@ -70,7 +70,7 @@ If it instead aborts with `Unresolved autopilot assignees: ...`, that is not fix
70
70
 
71
71
  The import also rewrites any `mention://agent/<id>` link inside squad and agent instructions (e.g. `[@dev-backend](mention://agent/<id>)`) from the source agent's id to its new id in the target workspace — the CLI does this automatically for every agent captured in the bundle; no extra flag needed. Mentions pointing to an agent outside the bundle are left untouched.
72
72
 
73
- Instructions are read back from each resource's sibling `.md` (`agents/<slug>.md`, `squads/<slug>.md`) when present editing that Markdown is the supported way to review and enhance an agent's or squad's instructions before import. Older bundles that predate the split (instructions inline in the JSON, no `instructions_file`) still import unchanged.
73
+ Prose fields are read back from each resource's sibling `.md` when a `*_file` key points at it — **instructions** from `<slug>.md` (agents, squads) and **description** from `<slug>.description.md` (agents, squads, projects, autopilots). Editing that Markdown is the supported way to review and enhance the prose before import. Older bundles that predate the split (prose inline in the JSON, no `*_file` key) still import unchanged.
74
74
 
75
75
  Avatars are restored automatically, but **only when the target resource has none** — an existing agent or squad that already carries an avatar is never overwritten. New agents get their bundled image re-uploaded; new squads get their `avatar_url` (emoji or URL) set. An agent whose source avatar was an emoji can't be restored (the CLI has no emoji setter for agents) and is reported as unsupported.
76
76
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "plugin-validator",
3
3
  "displayName": "Plugin Validator",
4
- "version": "0.0.43",
4
+ "version": "0.0.45",
5
5
  "description": "Orchestrated validator for Claude Code plugins — validates skills, agents, commands, and hooks across every plugin under plugins/**.",
6
6
  "author": {
7
7
  "name": "Steven Hoang"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "team-share",
3
3
  "displayName": "Team Share",
4
- "version": "0.0.43",
4
+ "version": "0.0.45",
5
5
  "description": "Onboard your team with an interactive setup menu: install CodeGraph, build the Understand-Anything knowledge graph, and share Claude Code settings — run any combination, all idempotent.",
6
6
  "author": {
7
7
  "name": "Steven Hoang"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tech-graph",
3
- "version": "0.0.43",
3
+ "version": "0.0.45",
4
4
  "description": "Step-by-step wizard for generating technical diagrams as SVG+PNG.",
5
5
  "author": {
6
6
  "name": "steven"