@compilr-dev/cli 0.8.9 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/agent.d.ts +9 -0
  3. package/dist/agent.js +1 -0
  4. package/dist/commands-v2/handlers/project.js +88 -229
  5. package/dist/commands-v2/handlers/skill.js +10 -6
  6. package/dist/commands-v2/handlers/team.d.ts +19 -1
  7. package/dist/commands-v2/handlers/team.js +178 -3
  8. package/dist/compilr-diff-companion.vsix +0 -0
  9. package/dist/index.js +55 -3
  10. package/dist/multi-agent/agent-edit.d.ts +75 -0
  11. package/dist/multi-agent/agent-edit.js +84 -0
  12. package/dist/multi-agent/capability-loader.d.ts +15 -5
  13. package/dist/multi-agent/capability-loader.js +18 -8
  14. package/dist/multi-agent/custom-agents.d.ts +12 -0
  15. package/dist/multi-agent/custom-agents.js +22 -1
  16. package/dist/repl-helpers.d.ts +6 -1
  17. package/dist/repl-helpers.js +24 -10
  18. package/dist/repl-v2.d.ts +10 -0
  19. package/dist/repl-v2.js +43 -2
  20. package/dist/ui/overlay/impl/agent-edit-overlay-v2.d.ts +105 -0
  21. package/dist/ui/overlay/impl/agent-edit-overlay-v2.js +364 -0
  22. package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.d.ts +1 -8
  23. package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.js +16 -120
  24. package/dist/ui/overlay/impl/project-edit-overlay-v2.d.ts +2 -1
  25. package/dist/ui/overlay/impl/project-edit-overlay-v2.js +26 -19
  26. package/dist/ui/overlay/impl/skills-overlay-v2.d.ts +42 -2
  27. package/dist/ui/overlay/impl/skills-overlay-v2.js +238 -185
  28. package/dist/ui/overlay/impl/team-overlay-v2.d.ts +1 -2
  29. package/dist/ui/overlay/impl/team-overlay-v2.js +6 -9
  30. package/dist/ui/terminal-types.d.ts +8 -0
  31. package/package.json +7 -3
@@ -6,9 +6,11 @@
6
6
  import pc from 'picocolors';
7
7
  import { TeamOverlayV2 } from '../../ui/overlay/impl/team-overlay-v2.js';
8
8
  import { CustomAgentFormOverlayV2 } from '../../ui/overlay/impl/custom-agent-form-overlay-v2.js';
9
+ import { AgentEditOverlayV2 } from '../../ui/overlay/impl/agent-edit-overlay-v2.js';
9
10
  import { ArtifactOverlayV2, ArtifactDetailOverlayV2 } from '../../ui/overlay/index.js';
10
11
  import { ROLE_METADATA, PREDEFINED_ROLE_IDS } from '../../multi-agent/index.js';
11
- import { loadCustomAgents, createCustomAgentDefinition, addCustomAgent, removeCustomAgent, } from '../../multi-agent/custom-agents.js';
12
+ import { loadCustomAgents, createCustomAgentDefinition, addCustomAgent, removeCustomAgent, updateCustomAgent, } from '../../multi-agent/custom-agents.js';
13
+ import { readEditableInstructions, buildAgentPatch, } from '../../multi-agent/agent-edit.js';
12
14
  import { getCurrentProject } from '../../tools/project-db.js';
13
15
  import { listTemplates, saveTemplate } from '@compilr-dev/sdk';
14
16
  import { getSessionRegistry } from '../../multi-agent/session-registry.js';
@@ -276,8 +278,182 @@ async function openTeamOverlay(ctx, team) {
276
278
  });
277
279
  }
278
280
  }
281
+ else if (result?.action === 'edit' && result.agentId) {
282
+ await handleEditAgent(ctx, team, result.agentId);
283
+ }
279
284
  return true;
280
285
  }
286
+ /**
287
+ * Edit an existing team agent.
288
+ *
289
+ * ⚠️ THE INSTRUCTIONS FIELD IS NOT THE SAME FIELD FOR BOTH AGENT KINDS, and reading the
290
+ * wrong one moves the user's data. A custom agent's live `systemPromptAddition` is the
291
+ * whole GENERATED prompt ("You are a X specialized in Y. …"), not the instructions the
292
+ * user typed — those live on the `CustomAgentDefinition` on disk. So:
293
+ *
294
+ * custom agent → read/write `CustomAgentDefinition.systemPromptAddition`, then
295
+ * regenerate the live prompt around it
296
+ * role agent → read/write the live `TeamAgent.systemPromptAddition`, which for that
297
+ * kind genuinely IS free text
298
+ *
299
+ * Loading one and saving the other is an asymmetric round trip: it does not look like a
300
+ * bug until the data has already moved.
301
+ */
302
+ // Exported for tests: the restoreAgent step below is the one failure in this flow that
303
+ // cannot be seen from the outside, so it is worth exercising directly.
304
+ export async function handleEditAgent(ctx, team, agentId) {
305
+ const teamAgent = team.get(agentId);
306
+ if (!teamAgent) {
307
+ ctx.ui.print({ type: 'error', message: `Agent '${agentId}' not found` });
308
+ return;
309
+ }
310
+ const isCustom = teamAgent.role === 'custom';
311
+ const projectPath = getCurrentProject()?.path;
312
+ /*
313
+ A custom agent's editable text lives in its definition; without the project we cannot
314
+ read it, and editing the generated prompt instead is the trap above. Path and definition
315
+ are kept together in one value so "this is a custom agent we can actually write" is a
316
+ single check later, rather than two that could drift apart.
317
+ */
318
+ let custom = null;
319
+ if (isCustom) {
320
+ if (!projectPath) {
321
+ ctx.ui.print({
322
+ type: 'warning',
323
+ message: 'Select a project first to edit custom agents',
324
+ });
325
+ return;
326
+ }
327
+ const definition = loadCustomAgents(projectPath).find((a) => a.id === agentId);
328
+ if (!definition) {
329
+ ctx.ui.print({
330
+ type: 'error',
331
+ message: `No stored definition for '${agentId}' — it cannot be edited from here.`,
332
+ });
333
+ return;
334
+ }
335
+ custom = { path: projectPath, definition };
336
+ }
337
+ const originals = {
338
+ displayName: teamAgent.displayName,
339
+ specialty: teamAgent.description ?? '',
340
+ instructions: readEditableInstructions(teamAgent.systemPromptAddition, custom?.definition),
341
+ mascot: teamAgent.mascot,
342
+ personality: teamAgent.personality ?? '',
343
+ modelTier: teamAgent.modelTier ?? 'balanced',
344
+ toolProfile: teamAgent.toolProfile ?? 'full',
345
+ systemPromptAddition: teamAgent.systemPromptAddition ?? '',
346
+ };
347
+ const overlay = new AgentEditOverlayV2({
348
+ agentId,
349
+ displayName: originals.displayName,
350
+ mascot: originals.mascot,
351
+ specialty: originals.specialty,
352
+ personality: originals.personality,
353
+ instructions: originals.instructions,
354
+ modelTier: originals.modelTier,
355
+ toolProfile: originals.toolProfile,
356
+ isCustom,
357
+ takenMascots: team
358
+ .getAll()
359
+ .filter((a) => a.id !== agentId)
360
+ .map((a) => a.mascot),
361
+ });
362
+ const result = await ctx.ui.showOverlay(overlay);
363
+ if (!result?.saved || !result.values) {
364
+ return;
365
+ }
366
+ const values = result.values;
367
+ /*
368
+ What should end up on the LIVE agent's prompt. For a custom agent that is the prompt
369
+ regenerated from the updated definition; for a role agent it is the edited free text.
370
+ */
371
+ let newSystemPromptAddition;
372
+ if (custom) {
373
+ /*
374
+ Write the definition first, then send the user's OWN words to the live agent.
375
+ SDK 0.23 composes the role scaffold, capability blurb, delegation guidance and
376
+ personality at `initialize()` from current state — so `systemPromptAddition` holds
377
+ user-authored text only. Regenerating a composite here and sending THAT would be
378
+ composed a second time on top.
379
+ */
380
+ const updatedDefinition = updateCustomAgent(custom.path, agentId, {
381
+ displayName: values.displayName,
382
+ specialty: values.specialty,
383
+ // Kept on the definition too, so the disk copy and the live agent do not drift.
384
+ personality: values.personality || undefined,
385
+ systemPromptAddition: values.instructions || undefined,
386
+ mascot: values.mascot,
387
+ modelTier: values.modelTier,
388
+ toolConfig: { ...custom.definition.toolConfig, profile: values.toolProfile },
389
+ });
390
+ if (!updatedDefinition) {
391
+ ctx.ui.print({
392
+ type: 'error',
393
+ message: `Could not write '${agentId}' to custom-agents.json — nothing was changed.`,
394
+ });
395
+ return;
396
+ }
397
+ /*
398
+ `?? ''` rather than leaving it undefined: clearing the instructions is a real edit,
399
+ and `buildAgentPatch` reads undefined as "unchanged" — so an emptied box would
400
+ silently keep the old text.
401
+ */
402
+ newSystemPromptAddition = updatedDefinition.systemPromptAddition ?? '';
403
+ }
404
+ else if (values.instructions !== originals.instructions) {
405
+ newSystemPromptAddition = values.instructions;
406
+ }
407
+ const patch = buildAgentPatch(originals, values, newSystemPromptAddition);
408
+ if (Object.keys(patch).length === 0) {
409
+ ctx.ui.print({ type: 'info', message: 'No changes.' });
410
+ return;
411
+ }
412
+ try {
413
+ const update = await team.updateAgent(agentId, patch);
414
+ syncTeamRoster(team);
415
+ /*
416
+ ⚠️ AN EDIT THAT DID NOT REACH DISK IS NOT A SAVE. The change is live in memory either
417
+ way, so the agent behaves as edited now and the loss only shows up on restart.
418
+ */
419
+ saveTeamAndSession(team, ctx);
420
+ ctx.ui.print({
421
+ type: 'success',
422
+ message: `Updated ${teamAgent.getFullLabel()}`,
423
+ });
424
+ /*
425
+ ⚠️ THE REPL HOLDS A DIRECT REFERENCE TO THE LIVE AGENT. A rebuild replaces the object
426
+ behind it, and without this the REPL keeps talking to the disposed one — so the edit
427
+ appears to do nothing, but only for the agent that happens to be active, which reads
428
+ as intermittent rather than broken. `updateAgent` rebuilds eagerly, so `.agent` is
429
+ already the new instance here.
430
+ */
431
+ if (update.rebuilt && agentId === team.activeAgentId && teamAgent.agent && ctx.restoreAgent) {
432
+ ctx.restoreAgent(teamAgent.agent);
433
+ }
434
+ // Capability changes the agent cannot observe have to reach the conversation: its own
435
+ // history has it planning things it may no longer be able to do.
436
+ if (update.lostWriteAccess) {
437
+ ctx.ui.print({
438
+ type: 'warning',
439
+ message: `${teamAgent.getFullLabel()} can no longer change files (${update.previousProfile ?? 'unrestricted'} → ${update.newProfile ?? 'unrestricted'}).`,
440
+ });
441
+ }
442
+ else if (update.toolsNarrowed) {
443
+ ctx.ui.print({
444
+ type: 'warning',
445
+ message: `${teamAgent.getFullLabel()} can now reach fewer tools (${update.previousProfile ?? 'unrestricted'} → ${update.newProfile ?? 'unrestricted'}).`,
446
+ });
447
+ }
448
+ }
449
+ catch (err) {
450
+ // The SDK throws on an unknown agent, an unknown profile, and any attempt to change id.
451
+ ctx.ui.print({
452
+ type: 'error',
453
+ message: `Failed to update agent: ${err instanceof Error ? err.message : 'Unknown error'}`,
454
+ });
455
+ }
456
+ }
281
457
  async function handleCreateCustomAgent(ctx, team) {
282
458
  // Check for project context
283
459
  const currentProject = getCurrentProject();
@@ -317,13 +493,12 @@ async function handleCreateCustomAgent(ctx, team) {
317
493
  mascot: '',
318
494
  createdAt: new Date().toISOString(),
319
495
  toolConfig: agentData.toolConfig,
320
- enabledSkills: agentData.enabledSkills,
321
496
  modelTier: agentData.modelTier,
322
497
  });
323
498
  ctx.ui.print({ type: 'success', message: `Template "${formResult.saveAsTemplate}" saved.` });
324
499
  }
325
500
  // Create the custom agent definition
326
- const agentDef = createCustomAgentDefinition(agentData.id, agentData.displayName, agentData.specialty, agentData.personality, existingCustomAgents, agentData.toolConfig, agentData.enabledSkills, agentData.modelTier, agentData.systemPromptAddition);
501
+ const agentDef = createCustomAgentDefinition(agentData.id, agentData.displayName, agentData.specialty, agentData.personality, existingCustomAgents, agentData.toolConfig, agentData.modelTier, agentData.systemPromptAddition);
327
502
  // Save to project
328
503
  try {
329
504
  addCustomAgent(projectPath, agentDef);
Binary file
package/dist/index.js CHANGED
@@ -15,6 +15,8 @@ import { perf, flushStartupPerf } from './utils/startup-perf.js';
15
15
  perf('imports-start');
16
16
  import { createAgent } from './agent.js';
17
17
  import { getModelForTier, getProviderMetadata } from './models/index.js';
18
+ import { filterMcpToolsByGrant, loadInstalledSkills, resolveSkillsForTeamAgent } from '@compilr-dev/sdk';
19
+ import { getProjectDir } from './repl-helpers.js';
18
20
  import { ReplV2 } from './repl-v2.js';
19
21
  import { AgentTeam, setActiveSharedContext } from './multi-agent/index.js';
20
22
  import { getStyles } from './themes/index.js';
@@ -657,7 +659,35 @@ async function main() {
657
659
  // ==========================================================================
658
660
  perf('team-setup-start');
659
661
  // Define agent factory for creating team agents
660
- const agentFactory = (_config, systemPromptAddition, useMinimalSystemPrompt, noTools, toolFilter, modelTier) => {
662
+ /**
663
+ * The skills an agent may CHOOSE — distinct from the slash commands a user types.
664
+ *
665
+ * ⚠️ THE HOST FILTERS, THE SDK PRESENTS, and `resolveSkillsForTeamAgent` holds every judgement
666
+ * so the CLI and Desktop cannot drift about what an absent grant means or which filters apply.
667
+ * `loadInstalledSkills` excludes the 42 platform prompts on purpose: those are Commands — modes
668
+ * a user forces — not capabilities a model selects.
669
+ */
670
+ const skillsForAgent = (teamAgent) => {
671
+ // No agent identity means no eligibility context, so no catalogue.
672
+ if (!teamAgent)
673
+ return [];
674
+ const pool = loadInstalledSkills(getProjectDir());
675
+ if (pool.length === 0)
676
+ return [];
677
+ return resolveSkillsForTeamAgent(pool, teamAgent, { toolNames: teamAgent.toolFilter });
678
+ };
679
+ /** The MCP tools an agent may be built with. See the note at the `mcpTools` call site. */
680
+ const grantedMcpTools = (tools, teamAgent) => {
681
+ if (tools.length === 0)
682
+ return undefined;
683
+ // No agent identity means no grant to honour — nothing is filtered, as before.
684
+ if (!teamAgent)
685
+ return tools;
686
+ const servers = mcpManagerInstance?.getServerNames() ?? [];
687
+ const granted = filterMcpToolsByGrant(tools, servers, (s) => teamAgent.grantsMcpServer(s));
688
+ return granted.length > 0 ? granted : undefined;
689
+ };
690
+ const agentFactory = (_config, systemPromptAddition, useMinimalSystemPrompt, noTools, toolFilter, modelTier, teamAgent) => {
661
691
  // Resolve model from tier if provided, otherwise use global model
662
692
  const agentModel = modelTier ? getModelForTier(provider, modelTier) : model;
663
693
  // Create new agent with same base options plus role-specific system prompt
@@ -701,7 +731,18 @@ async function main() {
701
731
  // Tool filter - limit which tools this agent can use
702
732
  toolFilter,
703
733
  // MCP tools from external servers (reads mutable ref — includes tools once MCP init completes)
704
- mcpTools: mcpToolsRef.length > 0 ? mcpToolsRef : undefined,
734
+ /*
735
+ ⚠️ MCP REACH IS PER AGENT, AND THE TOOL PROFILE NEVER GATED IT. MCP tools are in no
736
+ capability pack — discovered at runtime, they never could be — and the SDK allows
737
+ unpacked "orphan" tools, so a read-only agent could run an MCP WRITE tool. Filtering at
738
+ construction is the enforcement: a tool the agent is not built with cannot be called.
739
+
740
+ `grantsMcpServer` owns what an absent grant means (undefined = an agent from before
741
+ grants existed, which still reaches everything), and `filterMcpToolsByGrant` owns the
742
+ tool→server mapping, so the CLI cannot drift from Desktop on either.
743
+ */
744
+ mcpTools: grantedMcpTools(mcpToolsRef, teamAgent),
745
+ skills: skillsForAgent(teamAgent),
705
746
  // Suppress startup logs for team agents (avoid UI interference)
706
747
  quiet: true,
707
748
  // Episode recorder for work history tracking (team agents share project store)
@@ -982,9 +1023,20 @@ async function main() {
982
1023
  .then((tools) => {
983
1024
  perf('mcp-background-done');
984
1025
  replV2.setMCPLoading(false, tools.length);
1026
+ /*
1027
+ ⚠️ THE SECOND INJECTION PATH. `mcpToolsRef` feeds the factory, which filters by grant —
1028
+ but the main agent already exists by now and gets its tools here instead, straight onto
1029
+ its registry where no capability filter looks. Desktop had the same pair; filtering only
1030
+ the factory leaves grants working except for the agent you are already talking to.
1031
+
1032
+ `mcpToolsRef` stays UNFILTERED on purpose: it is the pool every agent is filtered FROM,
1033
+ and narrowing it here would apply the default agent's grant to the whole team.
1034
+ */
985
1035
  if (tools.length > 0) {
986
- agent?.registerTools(tools);
987
1036
  mcpToolsRef = tools;
1037
+ const forDefault = grantedMcpTools(tools, sharedState.team?.get('default'));
1038
+ if (forDefault && forDefault.length > 0)
1039
+ agent?.registerTools(forDefault);
988
1040
  }
989
1041
  })
990
1042
  .catch(() => {
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Agent edit — the decisions an edit makes, separated from the UI that collects it.
3
+ *
4
+ * These are pure so they can be tested. The two things here are the two things most
5
+ * likely to be silently wrong: WHICH prompt field an edit reads and writes, and WHAT
6
+ * ends up in the patch.
7
+ */
8
+ import type { TeamAgentUpdate, MascotExpression, ToolProfile } from '@compilr-dev/sdk';
9
+ import type { CustomAgentDefinition } from './custom-agents.js';
10
+ import type { ModelTier } from '../models/index.js';
11
+ /** The agent's values as they were when the editor opened. */
12
+ export interface AgentEditOriginals {
13
+ displayName: string;
14
+ specialty: string;
15
+ instructions: string;
16
+ mascot: MascotExpression;
17
+ modelTier: ModelTier;
18
+ toolProfile: ToolProfile;
19
+ /** Tone. Composed into the prompt at initialize() as "Your manner: …". */
20
+ personality: string;
21
+ /** The live agent's current prompt addition — user-authored text since SDK 0.23. */
22
+ systemPromptAddition: string;
23
+ }
24
+ /** The values the editor handed back. */
25
+ export interface AgentEditedValues {
26
+ displayName: string;
27
+ specialty: string;
28
+ instructions: string;
29
+ mascot: MascotExpression;
30
+ modelTier: ModelTier;
31
+ toolProfile: ToolProfile;
32
+ personality: string;
33
+ }
34
+ /**
35
+ * Which text is the user's to edit.
36
+ *
37
+ * ⚠️ THE TWO FIELDS SHARE A NAME AND DO NOT SHARE A MEANING.
38
+ * `CustomAgentDefinition.systemPromptAddition` is what the user typed
39
+ * ("Always cite the ADR."). `TeamAgent.systemPromptAddition` is the whole GENERATED
40
+ * prompt ("You are a Solution Architect specialized in …"), because
41
+ * `fromCustomDefinition` stores the generated composite there.
42
+ *
43
+ * So a custom agent's editable text comes from its definition, and a role agent's — which
44
+ * has no definition — comes from the live agent.
45
+ *
46
+ * ⚠️ Corrected: before SDK 0.23 the live field was NOT free text for a role agent either.
47
+ * `fromRole` composed the role scaffold, tool awareness and delegation guidance into it —
48
+ * 6,321 characters for `pm`. An earlier note here claimed role agents were unaffected; that
49
+ * was asserted from reading the code rather than running it, and it was wrong. Since 0.23
50
+ * the field holds user text for both kinds, so this fallback is now correct for both — but
51
+ * the definition stays authoritative for custom agents, because it is what is on disk.
52
+ */
53
+ export declare function readEditableInstructions(liveSystemPromptAddition: string | undefined, definition: CustomAgentDefinition | undefined): string;
54
+ /**
55
+ * Build the minimal patch for an edit.
56
+ *
57
+ * Only changed fields are included, so an untouched field never triggers a rebuild —
58
+ * `applyUpdate` compares each present key against the live value and rebuilds on any
59
+ * difference in the prompt, personality, profile, filter or skills.
60
+ *
61
+ * `newSystemPromptAddition` is the USER'S OWN WORDS, for both agent kinds, or undefined
62
+ * when the text was not touched.
63
+ *
64
+ * ⚠️ This changed in SDK 0.23. `systemPromptAddition` now holds user-authored text ONLY:
65
+ * the role scaffold, capability blurb, delegation guidance and personality are composed at
66
+ * `initialize()` from current state. Sending a pre-composed prompt here would be composed a
67
+ * second time on top of itself — and was previously the reason a tool-profile change could
68
+ * leave the prompt still claiming tools the filter had already removed.
69
+ *
70
+ * ⚠️ `toolProfile` is sent WITHOUT a `toolFilter`. The profile is a label; the filter is
71
+ * the enforcement, and `applyUpdate` derives the filter from the profile precisely when
72
+ * the filter is absent. Supplying our own would override that with a second source of
73
+ * truth for the same fact.
74
+ */
75
+ export declare function buildAgentPatch(originals: AgentEditOriginals, values: AgentEditedValues, newSystemPromptAddition: string | undefined): TeamAgentUpdate;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Agent edit — the decisions an edit makes, separated from the UI that collects it.
3
+ *
4
+ * These are pure so they can be tested. The two things here are the two things most
5
+ * likely to be silently wrong: WHICH prompt field an edit reads and writes, and WHAT
6
+ * ends up in the patch.
7
+ */
8
+ /**
9
+ * Which text is the user's to edit.
10
+ *
11
+ * ⚠️ THE TWO FIELDS SHARE A NAME AND DO NOT SHARE A MEANING.
12
+ * `CustomAgentDefinition.systemPromptAddition` is what the user typed
13
+ * ("Always cite the ADR."). `TeamAgent.systemPromptAddition` is the whole GENERATED
14
+ * prompt ("You are a Solution Architect specialized in …"), because
15
+ * `fromCustomDefinition` stores the generated composite there.
16
+ *
17
+ * So a custom agent's editable text comes from its definition, and a role agent's — which
18
+ * has no definition — comes from the live agent.
19
+ *
20
+ * ⚠️ Corrected: before SDK 0.23 the live field was NOT free text for a role agent either.
21
+ * `fromRole` composed the role scaffold, tool awareness and delegation guidance into it —
22
+ * 6,321 characters for `pm`. An earlier note here claimed role agents were unaffected; that
23
+ * was asserted from reading the code rather than running it, and it was wrong. Since 0.23
24
+ * the field holds user text for both kinds, so this fallback is now correct for both — but
25
+ * the definition stays authoritative for custom agents, because it is what is on disk.
26
+ */
27
+ export function readEditableInstructions(liveSystemPromptAddition, definition) {
28
+ if (definition) {
29
+ return definition.systemPromptAddition ?? '';
30
+ }
31
+ return liveSystemPromptAddition ?? '';
32
+ }
33
+ /**
34
+ * Build the minimal patch for an edit.
35
+ *
36
+ * Only changed fields are included, so an untouched field never triggers a rebuild —
37
+ * `applyUpdate` compares each present key against the live value and rebuilds on any
38
+ * difference in the prompt, personality, profile, filter or skills.
39
+ *
40
+ * `newSystemPromptAddition` is the USER'S OWN WORDS, for both agent kinds, or undefined
41
+ * when the text was not touched.
42
+ *
43
+ * ⚠️ This changed in SDK 0.23. `systemPromptAddition` now holds user-authored text ONLY:
44
+ * the role scaffold, capability blurb, delegation guidance and personality are composed at
45
+ * `initialize()` from current state. Sending a pre-composed prompt here would be composed a
46
+ * second time on top of itself — and was previously the reason a tool-profile change could
47
+ * leave the prompt still claiming tools the filter had already removed.
48
+ *
49
+ * ⚠️ `toolProfile` is sent WITHOUT a `toolFilter`. The profile is a label; the filter is
50
+ * the enforcement, and `applyUpdate` derives the filter from the profile precisely when
51
+ * the filter is absent. Supplying our own would override that with a second source of
52
+ * truth for the same fact.
53
+ */
54
+ export function buildAgentPatch(originals, values, newSystemPromptAddition) {
55
+ const patch = {};
56
+ if (values.displayName !== originals.displayName) {
57
+ patch.displayName = values.displayName;
58
+ }
59
+ if (values.specialty !== originals.specialty) {
60
+ patch.description = values.specialty;
61
+ }
62
+ if (values.mascot !== originals.mascot) {
63
+ patch.mascot = values.mascot;
64
+ }
65
+ if (values.modelTier !== originals.modelTier) {
66
+ patch.modelTier = values.modelTier;
67
+ }
68
+ if (values.toolProfile !== originals.toolProfile) {
69
+ patch.toolProfile = values.toolProfile;
70
+ }
71
+ /*
72
+ Unblocked in SDK 0.23. `fromCustomDefinition` now passes `def.personality` through, so
73
+ this REPLACES the agent's tone rather than adding a second, contradicting one beside a
74
+ `Your approach:` line baked into the base prompt.
75
+ */
76
+ if (values.personality !== originals.personality) {
77
+ patch.personality = values.personality;
78
+ }
79
+ if (newSystemPromptAddition !== undefined &&
80
+ newSystemPromptAddition !== originals.systemPromptAddition) {
81
+ patch.systemPromptAddition = newSystemPromptAddition;
82
+ }
83
+ return patch;
84
+ }
@@ -1,9 +1,19 @@
1
1
  /**
2
- * Capability Loader — Bridge between slash commands and the CapabilityManager.
2
+ * Capability Loader — bridge between MACROS and the CapabilityManager.
3
3
  *
4
- * Stores a module-level reference to the current CapabilityManager
5
- * (set by agent.ts during createAgent()). Provides a high-level function
6
- * for slash command handlers to load capability packs required by a skill.
4
+ * A macro declares the tools its text assumes (MACRO_REQUIREMENTS); this maps those to
5
+ * capability packs and loads them before the macro's prompt is queued, so `/design` arrives
6
+ * with the tools it is about to ask for.
7
+ *
8
+ * ⚠️ THIS WHOLE PATH IS DEAD, AND HAS BEEN SINCE IT WAS WRITTEN. `setCapabilityManager` has NO
9
+ * CALLERS anywhere in the CLI — the header below says it is "set by agent.ts during
10
+ * createAgent()", and agent.ts does not call it. So `currentManager` is permanently null,
11
+ * `loadCapabilitiesForMacro` returns at its first guard every time, and the warning it exists
12
+ * to print has never printed. Verified by grep, not by reading.
13
+ *
14
+ * Left in place rather than deleted because the design is sound and the tier flip that made
15
+ * packs lazy is what it was waiting for — but it is a decision, not an oversight to inherit.
16
+ * Wire it or delete it; do not assume it works.
7
17
  */
8
18
  import type { CapabilityManager, CapabilityLoadResult } from '@compilr-dev/sdk';
9
19
  /**
@@ -26,4 +36,4 @@ export declare function getCapabilityManager(): CapabilityManager | null;
26
36
  * @param skillName - Skill name (e.g., 'design', 'build', 'refactor')
27
37
  * @returns Load result, or null if capability loading is not active or skill has no requirements
28
38
  */
29
- export declare function loadCapabilitiesForSkill(skillName: string): CapabilityLoadResult | null;
39
+ export declare function loadCapabilitiesForMacro(skillName: string): CapabilityLoadResult | null;
@@ -1,11 +1,21 @@
1
1
  /**
2
- * Capability Loader — Bridge between slash commands and the CapabilityManager.
2
+ * Capability Loader — bridge between MACROS and the CapabilityManager.
3
3
  *
4
- * Stores a module-level reference to the current CapabilityManager
5
- * (set by agent.ts during createAgent()). Provides a high-level function
6
- * for slash command handlers to load capability packs required by a skill.
4
+ * A macro declares the tools its text assumes (MACRO_REQUIREMENTS); this maps those to
5
+ * capability packs and loads them before the macro's prompt is queued, so `/design` arrives
6
+ * with the tools it is about to ask for.
7
+ *
8
+ * ⚠️ THIS WHOLE PATH IS DEAD, AND HAS BEEN SINCE IT WAS WRITTEN. `setCapabilityManager` has NO
9
+ * CALLERS anywhere in the CLI — the header below says it is "set by agent.ts during
10
+ * createAgent()", and agent.ts does not call it. So `currentManager` is permanently null,
11
+ * `loadCapabilitiesForMacro` returns at its first guard every time, and the warning it exists
12
+ * to print has never printed. Verified by grep, not by reading.
13
+ *
14
+ * Left in place rather than deleted because the design is sound and the tier flip that made
15
+ * packs lazy is what it was waiting for — but it is a decision, not an oversight to inherit.
16
+ * Wire it or delete it; do not assume it works.
7
17
  */
8
- import { SKILL_REQUIREMENTS } from '@compilr-dev/sdk';
18
+ import { MACRO_REQUIREMENTS } from '@compilr-dev/sdk';
9
19
  // =============================================================================
10
20
  // Module-level state
11
21
  // =============================================================================
@@ -37,14 +47,14 @@ export function getCapabilityManager() {
37
47
  * @param skillName - Skill name (e.g., 'design', 'build', 'refactor')
38
48
  * @returns Load result, or null if capability loading is not active or skill has no requirements
39
49
  */
40
- export function loadCapabilitiesForSkill(skillName) {
50
+ export function loadCapabilitiesForMacro(skillName) {
41
51
  if (!currentManager) {
42
52
  return null;
43
53
  }
44
- if (!(skillName in SKILL_REQUIREMENTS)) {
54
+ if (!(skillName in MACRO_REQUIREMENTS)) {
45
55
  return null;
46
56
  }
47
- const requirements = SKILL_REQUIREMENTS[skillName];
57
+ const requirements = MACRO_REQUIREMENTS[skillName];
48
58
  // Combine required + optional tools
49
59
  const allTools = [
50
60
  ...requirements.required,
@@ -24,3 +24,15 @@ export declare function addCustomAgent(projectPath: string, agent: CustomAgentDe
24
24
  * Remove a custom agent by ID and save.
25
25
  */
26
26
  export declare function removeCustomAgent(projectPath: string, agentId: string): boolean;
27
+ /**
28
+ * Update a custom agent's definition in place and save.
29
+ *
30
+ * Returns the updated definition (the caller needs it to regenerate the agent's
31
+ * system prompt), or `null` if no agent with that id is stored.
32
+ *
33
+ * ⚠️ `id` and `createdAt` are not patchable, by type rather than by a runtime throw.
34
+ * The id is persisted in message history as literal text — mentions resolve when
35
+ * typed, not when read — so renaming `$arch` would strand every prior reference.
36
+ * `AgentTeam.updateAgent` enforces the same constraint on the live agent.
37
+ */
38
+ export declare function updateCustomAgent(projectPath: string, agentId: string, patch: Partial<Omit<CustomAgentDefinition, 'id' | 'createdAt'>>): CustomAgentDefinition | null;
@@ -37,7 +37,6 @@ function migrateAgentV1toV2(agent) {
37
37
  return {
38
38
  ...agent,
39
39
  toolConfig: agent.toolConfig ?? createDefaultToolConfig(),
40
- enabledSkills: agent.enabledSkills ?? [],
41
40
  };
42
41
  }
43
42
  /**
@@ -99,3 +98,25 @@ export function removeCustomAgent(projectPath, agentId) {
99
98
  saveCustomAgents(projectPath, agents);
100
99
  return true;
101
100
  }
101
+ /**
102
+ * Update a custom agent's definition in place and save.
103
+ *
104
+ * Returns the updated definition (the caller needs it to regenerate the agent's
105
+ * system prompt), or `null` if no agent with that id is stored.
106
+ *
107
+ * ⚠️ `id` and `createdAt` are not patchable, by type rather than by a runtime throw.
108
+ * The id is persisted in message history as literal text — mentions resolve when
109
+ * typed, not when read — so renaming `$arch` would strand every prior reference.
110
+ * `AgentTeam.updateAgent` enforces the same constraint on the live agent.
111
+ */
112
+ export function updateCustomAgent(projectPath, agentId, patch) {
113
+ const agents = loadCustomAgents(projectPath);
114
+ const index = agents.findIndex((a) => a.id === agentId);
115
+ if (index === -1) {
116
+ return null;
117
+ }
118
+ const updated = { ...agents[index], ...patch };
119
+ agents[index] = updated;
120
+ saveCustomAgents(projectPath, agents);
121
+ return updated;
122
+ }
@@ -5,6 +5,7 @@
5
5
  * and improve maintainability. These functions support the REPL but don't
6
6
  * depend on REPL instance state.
7
7
  */
8
+ import { type SkillSources } from '@compilr-dev/sdk';
8
9
  import type { BacklogItem } from './tools/backlog.js';
9
10
  /**
10
11
  * Keyword patterns for tool selection based on user input.
@@ -38,9 +39,13 @@ export declare function detectCompilrProject(): CompilrProjectInfo;
38
39
  * Check if backlog has any items using the shared parser.
39
40
  */
40
41
  export declare function hasBacklogItems(backlogPath: string | null): boolean;
42
+ /** Get the current project directory (best effort). */
43
+ export declare function getProjectDir(): string | undefined;
44
+ /** Get CLI skill sources (factory + coding). */
45
+ export declare function getCliSkillSources(): SkillSources;
41
46
  /**
42
47
  * Get skill prompt by name. Delegates to SDK resolveSkillPrompt, minus any
43
- * Desktop-only SDK skills (see DESKTOP_ONLY_SKILLS).
48
+ * Desktop-only SDK skills (see DESKTOP_ONLY_MACROS).
44
49
  */
45
50
  export declare function getSkillPrompt(name: string): string | null;
46
51
  /**