stagent 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +44 -31
  2. package/dist/cli.js +24 -0
  3. package/docs/.coverage-gaps.json +154 -24
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +12 -2
  6. package/docs/features/chat.md +40 -5
  7. package/docs/features/cost-usage.md +1 -1
  8. package/docs/features/documents.md +5 -2
  9. package/docs/features/inbox-notifications.md +10 -2
  10. package/docs/features/keyboard-navigation.md +12 -3
  11. package/docs/features/provider-runtimes.md +16 -2
  12. package/docs/features/settings.md +2 -2
  13. package/docs/features/shared-components.md +7 -3
  14. package/docs/features/tables.md +3 -1
  15. package/docs/features/tool-permissions.md +6 -2
  16. package/docs/features/workflows.md +6 -2
  17. package/docs/getting-started.md +1 -1
  18. package/docs/index.md +1 -1
  19. package/docs/journeys/developer.md +25 -2
  20. package/docs/journeys/personal-use.md +12 -5
  21. package/docs/journeys/power-user.md +45 -14
  22. package/docs/journeys/work-use.md +17 -8
  23. package/docs/manifest.json +15 -15
  24. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +2 -2
  25. package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
  26. package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
  27. package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
  28. package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
  29. package/next.config.mjs +1 -0
  30. package/package.json +3 -3
  31. package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
  32. package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
  33. package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
  34. package/src/app/api/chat/export/route.ts +52 -0
  35. package/src/app/api/chat/files/search/route.ts +50 -0
  36. package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
  37. package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
  38. package/src/app/api/environment/skills/route.ts +13 -0
  39. package/src/app/api/schedules/[id]/execute/route.ts +2 -2
  40. package/src/app/api/settings/chat/pins/route.ts +94 -0
  41. package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
  42. package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
  43. package/src/app/api/settings/environment/route.ts +26 -0
  44. package/src/app/api/tasks/[id]/execute/route.ts +52 -12
  45. package/src/app/api/tasks/[id]/respond/route.ts +31 -15
  46. package/src/app/api/tasks/[id]/resume/route.ts +24 -3
  47. package/src/app/documents/page.tsx +4 -1
  48. package/src/app/settings/page.tsx +2 -0
  49. package/src/components/book/content-blocks.tsx +1 -1
  50. package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
  51. package/src/components/chat/__tests__/chat-session-provider.test.tsx +166 -1
  52. package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
  53. package/src/components/chat/capability-banner.tsx +68 -0
  54. package/src/components/chat/chat-command-popover.tsx +668 -47
  55. package/src/components/chat/chat-input.tsx +103 -8
  56. package/src/components/chat/chat-message.tsx +12 -3
  57. package/src/components/chat/chat-session-provider.tsx +73 -3
  58. package/src/components/chat/chat-shell.tsx +62 -3
  59. package/src/components/chat/command-tab-bar.tsx +68 -0
  60. package/src/components/chat/conversation-template-picker.tsx +421 -0
  61. package/src/components/chat/help-dialog.tsx +39 -0
  62. package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
  63. package/src/components/chat/skill-row.tsx +147 -0
  64. package/src/components/documents/document-browser.tsx +37 -19
  65. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
  66. package/src/components/notifications/permission-response-actions.tsx +155 -1
  67. package/src/components/playbook/playbook-detail-view.tsx +1 -1
  68. package/src/components/settings/environment-section.tsx +102 -0
  69. package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
  70. package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
  71. package/src/components/shared/command-palette.tsx +262 -2
  72. package/src/components/shared/filter-hint.tsx +70 -0
  73. package/src/components/shared/filter-input.tsx +59 -0
  74. package/src/components/shared/saved-searches-manager.tsx +199 -0
  75. package/src/components/tasks/task-bento-grid.tsx +12 -2
  76. package/src/components/tasks/task-card.tsx +3 -0
  77. package/src/components/tasks/task-chip-bar.tsx +30 -1
  78. package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
  79. package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
  80. package/src/hooks/use-active-skills.ts +110 -0
  81. package/src/hooks/use-chat-autocomplete.ts +120 -7
  82. package/src/hooks/use-enriched-skills.ts +19 -0
  83. package/src/hooks/use-pinned-entries.ts +104 -0
  84. package/src/hooks/use-recent-user-messages.ts +19 -0
  85. package/src/hooks/use-saved-searches.ts +142 -0
  86. package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
  87. package/src/lib/agents/__tests__/claude-agent.test.ts +17 -4
  88. package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
  89. package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
  90. package/src/lib/agents/claude-agent.ts +105 -46
  91. package/src/lib/agents/handoff/bus.ts +2 -2
  92. package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
  93. package/src/lib/agents/profiles/__tests__/registry.test.ts +47 -0
  94. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +30 -3
  95. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +6 -2
  96. package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
  97. package/src/lib/agents/profiles/registry.ts +97 -22
  98. package/src/lib/agents/profiles/types.ts +7 -1
  99. package/src/lib/agents/router.ts +3 -6
  100. package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
  101. package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
  102. package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
  103. package/src/lib/agents/runtime/catalog.ts +121 -0
  104. package/src/lib/agents/runtime/claude-sdk.ts +32 -0
  105. package/src/lib/agents/runtime/execution-target.ts +456 -0
  106. package/src/lib/agents/runtime/index.ts +4 -0
  107. package/src/lib/agents/runtime/launch-failure.ts +101 -0
  108. package/src/lib/agents/runtime/openai-codex.ts +35 -0
  109. package/src/lib/agents/runtime/openai-direct.ts +8 -0
  110. package/src/lib/agents/task-dispatch.ts +220 -0
  111. package/src/lib/agents/tool-permissions.ts +16 -1
  112. package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
  113. package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
  114. package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
  115. package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
  116. package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
  117. package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
  118. package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
  119. package/src/lib/chat/__tests__/types.test.ts +28 -0
  120. package/src/lib/chat/active-skills.ts +31 -0
  121. package/src/lib/chat/clean-filter-input.ts +30 -0
  122. package/src/lib/chat/codex-engine.ts +30 -7
  123. package/src/lib/chat/command-tabs.ts +61 -0
  124. package/src/lib/chat/context-builder.ts +141 -1
  125. package/src/lib/chat/dismissals.ts +73 -0
  126. package/src/lib/chat/engine.ts +109 -15
  127. package/src/lib/chat/files/__tests__/search.test.ts +135 -0
  128. package/src/lib/chat/files/expand-mention.ts +76 -0
  129. package/src/lib/chat/files/search.ts +99 -0
  130. package/src/lib/chat/skill-composition.ts +210 -0
  131. package/src/lib/chat/skill-conflict.ts +105 -0
  132. package/src/lib/chat/stagent-tools.ts +6 -19
  133. package/src/lib/chat/stream-telemetry.ts +9 -4
  134. package/src/lib/chat/system-prompt.ts +22 -0
  135. package/src/lib/chat/tool-catalog.ts +33 -3
  136. package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
  137. package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
  138. package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
  139. package/src/lib/chat/tools/__tests__/task-tools.test.ts +47 -0
  140. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +134 -0
  141. package/src/lib/chat/tools/blueprint-tools.ts +190 -0
  142. package/src/lib/chat/tools/helpers.ts +2 -0
  143. package/src/lib/chat/tools/profile-tools.ts +120 -23
  144. package/src/lib/chat/tools/skill-tools.ts +183 -0
  145. package/src/lib/chat/tools/task-tools.ts +6 -2
  146. package/src/lib/chat/tools/workflow-tools.ts +61 -20
  147. package/src/lib/chat/types.ts +15 -0
  148. package/src/lib/constants/settings.ts +2 -0
  149. package/src/lib/data/clear.ts +2 -6
  150. package/src/lib/db/bootstrap.ts +17 -0
  151. package/src/lib/db/schema.ts +26 -0
  152. package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
  153. package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
  154. package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
  155. package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
  156. package/src/lib/environment/data.ts +9 -0
  157. package/src/lib/environment/list-skills.ts +176 -0
  158. package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
  159. package/src/lib/environment/parsers/skill.ts +26 -5
  160. package/src/lib/environment/profile-generator.ts +56 -2
  161. package/src/lib/environment/skill-enrichment.ts +106 -0
  162. package/src/lib/environment/skill-recommendations.ts +66 -0
  163. package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
  164. package/src/lib/filters/__tests__/parse.test.ts +135 -0
  165. package/src/lib/filters/parse.ts +86 -0
  166. package/src/lib/instance/__tests__/detect.test.ts +1 -1
  167. package/src/lib/instance/__tests__/upgrade-poller.test.ts +50 -0
  168. package/src/lib/instance/fingerprint.ts +8 -10
  169. package/src/lib/instance/upgrade-poller.ts +53 -1
  170. package/src/lib/schedules/scheduler.ts +4 -4
  171. package/src/lib/utils/stagent-paths.ts +4 -0
  172. package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
  173. package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
  174. package/src/lib/workflows/blueprints/types.ts +6 -0
  175. package/src/lib/workflows/engine.ts +5 -3
  176. package/src/test/setup.ts +10 -0
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Shared composition service — single source of truth for activate_skill /
3
+ * deactivate_skill logic, used by both the chat tool handler and the thin
4
+ * HTTP routes (POST /api/chat/conversations/[id]/skills/activate|deactivate).
5
+ *
6
+ * The chat tool delegates here so the UI can reach the same behaviour over
7
+ * HTTP without going through MCP.
8
+ *
9
+ * See `features/chat-composition-ui-v1.md`.
10
+ */
11
+
12
+ import { eq } from "drizzle-orm";
13
+ import { db } from "@/lib/db";
14
+ import { conversations } from "@/lib/db/schema";
15
+ import { mergeActiveSkillIds } from "@/lib/chat/active-skills";
16
+ import type { SkillConflict } from "@/lib/chat/skill-conflict";
17
+
18
+ export type ActivateSkillResult =
19
+ | {
20
+ kind: "ok";
21
+ activatedSkillId: string;
22
+ activeSkillIds: string[];
23
+ skillName: string;
24
+ note?: string;
25
+ }
26
+ | {
27
+ kind: "conflicts";
28
+ activeSkillIds: string[];
29
+ conflicts: SkillConflict[];
30
+ hint: string;
31
+ }
32
+ | { kind: "error"; message: string };
33
+
34
+ export type DeactivateSkillResult =
35
+ | { kind: "ok"; previousSkillId: string | null }
36
+ | { kind: "error"; message: string };
37
+
38
+ /**
39
+ * Activate a skill on a conversation, respecting runtime composition limits.
40
+ *
41
+ * @param conversationId Target conversation.
42
+ * @param skillId Opaque skill id from list_skills.
43
+ * @param mode "replace" (default) clears prior active skills; "add"
44
+ * appends — runtime must support composition.
45
+ * @param force When mode="add", skip conflict heuristic warnings.
46
+ */
47
+ export async function activateSkill(args: {
48
+ conversationId: string;
49
+ skillId: string;
50
+ mode?: "replace" | "add";
51
+ force?: boolean;
52
+ }): Promise<ActivateSkillResult> {
53
+ const { conversationId, skillId, mode = "replace", force = false } = args;
54
+
55
+ try {
56
+ const { getSkill } = await import("@/lib/environment/list-skills");
57
+ const skill = getSkill(skillId);
58
+ if (!skill) return { kind: "error", message: `Skill not found: ${skillId}` };
59
+
60
+ const existing = await db
61
+ .select({
62
+ id: conversations.id,
63
+ activeSkillId: conversations.activeSkillId,
64
+ activeSkillIds: conversations.activeSkillIds,
65
+ runtimeId: conversations.runtimeId,
66
+ })
67
+ .from(conversations)
68
+ .where(eq(conversations.id, conversationId))
69
+ .get();
70
+
71
+ if (!existing) {
72
+ return { kind: "error", message: `Conversation not found: ${conversationId}` };
73
+ }
74
+
75
+ if (mode === "add") {
76
+ const { getRuntimeFeatures } = await import("@/lib/agents/runtime/catalog");
77
+ let features;
78
+ try {
79
+ features = getRuntimeFeatures(
80
+ existing.runtimeId as Parameters<typeof getRuntimeFeatures>[0]
81
+ );
82
+ } catch {
83
+ return {
84
+ kind: "error",
85
+ message: `Unknown runtime '${existing.runtimeId ?? "(none)"}' — cannot determine composition support`,
86
+ };
87
+ }
88
+
89
+ if (!features.supportsSkillComposition) {
90
+ return {
91
+ kind: "error",
92
+ message: `Runtime '${existing.runtimeId}' does not support skill composition — switch to a Claude/Codex/direct runtime to compose skills`,
93
+ };
94
+ }
95
+
96
+ const currentIds = mergeActiveSkillIds(existing.activeSkillId, existing.activeSkillIds);
97
+
98
+ if (currentIds.includes(skillId)) {
99
+ return {
100
+ kind: "ok",
101
+ activatedSkillId: skillId,
102
+ activeSkillIds: currentIds,
103
+ skillName: skill.name,
104
+ note: "skill already active",
105
+ };
106
+ }
107
+
108
+ if (currentIds.length >= features.maxActiveSkills) {
109
+ return {
110
+ kind: "error",
111
+ message: `Max active skills (${features.maxActiveSkills}) reached on '${existing.runtimeId}' — deactivate one first`,
112
+ };
113
+ }
114
+
115
+ if (!force && currentIds.length > 0) {
116
+ const { detectSkillConflicts } = await import("@/lib/chat/skill-conflict");
117
+ const allConflicts: SkillConflict[] = [];
118
+ for (const otherId of currentIds) {
119
+ const other = getSkill(otherId);
120
+ if (!other) continue;
121
+ const conflicts = detectSkillConflicts(
122
+ { id: skill.id, name: skill.name, content: skill.content },
123
+ { id: other.id, name: other.name, content: other.content }
124
+ );
125
+ allConflicts.push(...conflicts);
126
+ }
127
+ if (allConflicts.length > 0) {
128
+ return {
129
+ kind: "conflicts",
130
+ activeSkillIds: currentIds,
131
+ conflicts: allConflicts,
132
+ hint: "Re-call with force=true to add anyway",
133
+ };
134
+ }
135
+ }
136
+
137
+ // Append: store ALL composed IDs in the new column. Keep legacy
138
+ // activeSkillId as-is so single-skill read paths still work.
139
+ const newComposed = [...(existing.activeSkillIds ?? []), skillId];
140
+ await db
141
+ .update(conversations)
142
+ .set({ activeSkillIds: newComposed, updatedAt: new Date() })
143
+ .where(eq(conversations.id, conversationId));
144
+
145
+ return {
146
+ kind: "ok",
147
+ activatedSkillId: skillId,
148
+ activeSkillIds: mergeActiveSkillIds(existing.activeSkillId, newComposed),
149
+ skillName: skill.name,
150
+ };
151
+ }
152
+
153
+ // mode === "replace" (legacy / default)
154
+ await db
155
+ .update(conversations)
156
+ .set({
157
+ activeSkillId: skillId,
158
+ activeSkillIds: [],
159
+ updatedAt: new Date(),
160
+ })
161
+ .where(eq(conversations.id, conversationId));
162
+
163
+ return {
164
+ kind: "ok",
165
+ activatedSkillId: skillId,
166
+ activeSkillIds: [skillId],
167
+ skillName: skill.name,
168
+ };
169
+ } catch (e) {
170
+ return {
171
+ kind: "error",
172
+ message: e instanceof Error ? e.message : "activate_skill failed",
173
+ };
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Clear the active skill (and composed skills) on a conversation.
179
+ */
180
+ export async function deactivateSkill(args: {
181
+ conversationId: string;
182
+ }): Promise<DeactivateSkillResult> {
183
+ const { conversationId } = args;
184
+ try {
185
+ const existing = await db
186
+ .select({
187
+ id: conversations.id,
188
+ activeSkillId: conversations.activeSkillId,
189
+ })
190
+ .from(conversations)
191
+ .where(eq(conversations.id, conversationId))
192
+ .get();
193
+
194
+ if (!existing) {
195
+ return { kind: "error", message: `Conversation not found: ${conversationId}` };
196
+ }
197
+
198
+ await db
199
+ .update(conversations)
200
+ .set({ activeSkillId: null, activeSkillIds: [], updatedAt: new Date() })
201
+ .where(eq(conversations.id, conversationId));
202
+
203
+ return { kind: "ok", previousSkillId: existing.activeSkillId ?? null };
204
+ } catch (e) {
205
+ return {
206
+ kind: "error",
207
+ message: e instanceof Error ? e.message : "deactivate_skill failed",
208
+ };
209
+ }
210
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Lightweight heuristic that flags when two SKILL.md bodies issue
3
+ * divergent directives on the same topic. Pure function — no I/O.
4
+ *
5
+ * Approach (v1):
6
+ * 1. For each skill, extract "directive lines" containing one of:
7
+ * always | never | must | prefer | use | avoid | don't | do not
8
+ * 2. Tokenize each directive into content words (lowercase, drop
9
+ * stopwords + the directive verb itself).
10
+ * 3. For each pair of directives across the two skills with significant
11
+ * keyword overlap (≥2 shared content words ≥4 chars), check if their
12
+ * directive verbs disagree (always vs never, prefer vs avoid, etc.).
13
+ * 4. Surface the disagreeing pair as a SkillConflict.
14
+ *
15
+ * False positives are acceptable — the consumer presents excerpts to the
16
+ * user, not a binary block. False negatives (semantic conflict without
17
+ * keyword overlap) await the embedding-based v2.
18
+ */
19
+
20
+ export interface SkillMarkdown {
21
+ id: string;
22
+ name: string;
23
+ content: string;
24
+ }
25
+
26
+ export interface SkillConflict {
27
+ skillA: string; // skill name
28
+ skillB: string; // skill name
29
+ sharedTopic: string; // joined keywords that overlapped
30
+ excerptA: string; // the directive line from A
31
+ excerptB: string; // the directive line from B
32
+ }
33
+
34
+ const POSITIVE_DIRECTIVES = new Set(["always", "must", "prefer", "use", "do"]);
35
+ const NEGATIVE_DIRECTIVES = new Set(["never", "avoid", "don't", "dont", "skip"]);
36
+ const ALL_DIRECTIVES = new Set([...POSITIVE_DIRECTIVES, ...NEGATIVE_DIRECTIVES]);
37
+
38
+ const STOPWORDS = new Set([
39
+ "the", "a", "an", "and", "or", "but", "for", "with", "without",
40
+ "to", "of", "in", "on", "at", "by", "from", "as", "is", "are",
41
+ "be", "this", "that", "these", "those", "it", "its", "before",
42
+ "after", "during", "into", "out",
43
+ ]);
44
+
45
+ interface DirectiveLine {
46
+ raw: string;
47
+ polarity: "positive" | "negative";
48
+ keywords: Set<string>;
49
+ }
50
+
51
+ function extractDirectives(content: string): DirectiveLine[] {
52
+ const lines = content.split(/\r?\n/);
53
+ const out: DirectiveLine[] = [];
54
+ for (const rawLine of lines) {
55
+ const line = rawLine.trim();
56
+ if (!line || line.startsWith("#") || line.startsWith("```")) continue;
57
+ const lower = line.toLowerCase();
58
+ let polarity: "positive" | "negative" | null = null;
59
+ for (const tok of lower.split(/\s+/)) {
60
+ const cleaned = tok.replace(/[^a-z']/g, "");
61
+ if (POSITIVE_DIRECTIVES.has(cleaned)) { polarity = "positive"; break; }
62
+ if (NEGATIVE_DIRECTIVES.has(cleaned)) { polarity = "negative"; break; }
63
+ }
64
+ if (!polarity) continue;
65
+ const keywords = new Set<string>();
66
+ for (const tok of lower.split(/[^a-z0-9]+/)) {
67
+ if (tok.length < 4) continue;
68
+ if (STOPWORDS.has(tok) || ALL_DIRECTIVES.has(tok)) continue;
69
+ keywords.add(tok);
70
+ }
71
+ if (keywords.size === 0) continue;
72
+ out.push({ raw: line, polarity, keywords });
73
+ }
74
+ return out;
75
+ }
76
+
77
+ function intersect(a: Set<string>, b: Set<string>): string[] {
78
+ const out: string[] = [];
79
+ for (const tok of a) if (b.has(tok)) out.push(tok);
80
+ return out;
81
+ }
82
+
83
+ export function detectSkillConflicts(
84
+ a: SkillMarkdown,
85
+ b: SkillMarkdown
86
+ ): SkillConflict[] {
87
+ const directivesA = extractDirectives(a.content);
88
+ const directivesB = extractDirectives(b.content);
89
+ const conflicts: SkillConflict[] = [];
90
+ for (const da of directivesA) {
91
+ for (const db of directivesB) {
92
+ if (da.polarity === db.polarity) continue;
93
+ const shared = intersect(da.keywords, db.keywords);
94
+ if (shared.length < 2) continue;
95
+ conflicts.push({
96
+ skillA: a.name,
97
+ skillB: b.name,
98
+ sharedTopic: shared.slice(0, 3).join(", "),
99
+ excerptA: da.raw,
100
+ excerptB: db.raw,
101
+ });
102
+ }
103
+ }
104
+ return conflicts;
105
+ }
@@ -21,6 +21,8 @@ import { chatHistoryTools } from "./tools/chat-history-tools";
21
21
  import { handoffTools } from "./tools/handoff-tools";
22
22
  import { tableTools } from "./tools/table-tools";
23
23
  import { runtimeTools } from "./tools/runtime-tools";
24
+ import { blueprintTools } from "./tools/blueprint-tools";
25
+ import { skillTools } from "./tools/skill-tools";
24
26
 
25
27
 
26
28
  // ── Tool server types ────────────────────────────────────────────────
@@ -57,6 +59,8 @@ function collectAllTools(ctx: ToolContext): ToolDefinition[] {
57
59
  ...handoffTools(ctx),
58
60
  ...tableTools(ctx),
59
61
  ...runtimeTools(ctx),
62
+ ...blueprintTools(ctx),
63
+ ...skillTools(ctx),
60
64
  ];
61
65
  }
62
66
 
@@ -71,8 +75,9 @@ function collectAllTools(ctx: ToolContext): ToolDefinition[] {
71
75
  export function createToolServer(
72
76
  projectId?: string | null,
73
77
  onToolResult?: (toolName: string, result: unknown) => void,
78
+ projectDir?: string | null,
74
79
  ): ToolServer {
75
- const ctx: ToolContext = { projectId, onToolResult };
80
+ const ctx: ToolContext = { projectId, projectDir, onToolResult };
76
81
  const allTools = collectAllTools(ctx);
77
82
 
78
83
  // Handler lookup map (built once, shared across modes)
@@ -114,21 +119,3 @@ export function createToolServer(
114
119
  };
115
120
  }
116
121
 
117
- // ── Backward-compatible export ───────────────────────────────────────
118
-
119
- /**
120
- * Create an in-process MCP server exposing all Stagent tools.
121
- * The `projectId` closure auto-scopes operations to the active project.
122
- * `onToolResult` is called after each successful CRUD operation with the
123
- * tool name and returned entity data — used by the entity detector to
124
- * generate deterministic Quick Access navigation links.
125
- *
126
- * @deprecated Use `createToolServer()` for new code. This wrapper exists
127
- * for backward compatibility with the chat engine.
128
- */
129
- export function createStagentMcpServer(
130
- projectId?: string | null,
131
- onToolResult?: (toolName: string, result: unknown) => void,
132
- ) {
133
- return createToolServer(projectId, onToolResult).asMcpServer();
134
- }
@@ -23,11 +23,16 @@
23
23
  * - stream.reconciled.stale — reconcileStreamingMessages swept an orphan
24
24
  * at chat page load (10-min cutoff)
25
25
  *
26
- * Three client-side reason codes (logged via console.info with a stable
26
+ * Four client-side reason codes (logged via console.info with a stable
27
27
  * prefix so tests and grep can find them):
28
- * - client.stream.done — reader.read() returned done: true
29
- * - client.stream.user-abort — user clicked Stop / AbortController fired
30
- * - client.stream.reader-error — reader.read() or decode threw
28
+ * - client.stream.done — reader.read() returned done: true
29
+ * - client.stream.user-abort — user clicked Stop / AbortController fired
30
+ * - client.stream.reader-error — reader.read() or decode threw
31
+ * - client.stream.view-remount — a chat-consuming component unmounted
32
+ * while a stream was in flight. The stream
33
+ * itself continues in the provider; this
34
+ * code exists so diagnostics can confirm
35
+ * the provider-hoisting fix is holding.
31
36
  *
32
37
  * As of the `chat-session-persistence-provider` feature, the SSE reader
33
38
  * loop runs inside `ChatSessionProvider` (rendered from the root layout),
@@ -1,6 +1,28 @@
1
1
  /**
2
2
  * Enhanced system prompt for the Stagent chat LLM.
3
3
  * Provides identity, tool catalog, and intent routing guidance.
4
+ *
5
+ * ## Tier 0 vs CLAUDE.md partition (DD-CE-002)
6
+ *
7
+ * When the chat engine runs on the `claude-code` runtime, the Claude Agent
8
+ * SDK loads project-level `CLAUDE.md` and user-level `~/.claude/CLAUDE.md`
9
+ * via `settingSources: ["user", "project"]`. To avoid double-prompting,
10
+ * this system prompt MUST stay scoped to:
11
+ *
12
+ * (a) Stagent identity
13
+ * (b) Stagent tool catalog and routing
14
+ * (c) Stagent domain semantics (delay steps, enrich_table, workflow dedup)
15
+ * (d) LLM interaction style
16
+ *
17
+ * Content that is project-specific (coding conventions, testing rules,
18
+ * git workflow, repo-specific gotchas) belongs in `CLAUDE.md` — NOT here.
19
+ *
20
+ * Audit (2026-04-13): every current block in this prompt passes the rubric.
21
+ * No content migration was required for Stagent's current CLAUDE.md state.
22
+ * The worktree note on line 110 is borderline and flagged for revisit if
23
+ * CLAUDE.md gains an explicit worktree section.
24
+ *
25
+ * Reference: features/chat-claude-sdk-skills.md (§"Tier 0 vs CLAUDE.md").
4
26
  */
5
27
 
6
28
  export const STAGENT_SYSTEM_PROMPT = `You are Stagent, an AI workspace assistant for managing software projects, tasks, workflows, documents, and schedules. You are a full alternate UI for the Stagent app — users can do everything through chat that they can do in the GUI.
@@ -13,6 +13,7 @@ import {
13
13
  Sun,
14
14
  Sparkles,
15
15
  Table2,
16
+ Zap,
16
17
  } from "lucide-react";
17
18
  import type { LucideIcon } from "lucide-react";
18
19
 
@@ -32,7 +33,8 @@ export type ToolGroup =
32
33
  | "Settings"
33
34
  | "Chat"
34
35
  | "Browser"
35
- | "Utility";
36
+ | "Utility"
37
+ | "Session";
36
38
 
37
39
  export interface ToolCatalogEntry {
38
40
  /** MCP tool name, e.g. "list_tasks" */
@@ -50,6 +52,7 @@ export interface ToolCatalogEntry {
50
52
  // ── Group → Icon mapping ─────────────────────────────────────────────────
51
53
 
52
54
  export const TOOL_GROUP_ICONS: Record<ToolGroup, LucideIcon> = {
55
+ Session: Zap,
53
56
  Tasks: ListTodo,
54
57
  Projects: FolderKanban,
55
58
  Workflows: GitBranch,
@@ -68,6 +71,7 @@ export const TOOL_GROUP_ICONS: Record<ToolGroup, LucideIcon> = {
68
71
 
69
72
  /** Display order for groups in the popover */
70
73
  export const TOOL_GROUP_ORDER: ToolGroup[] = [
74
+ "Session",
71
75
  "Tasks",
72
76
  "Projects",
73
77
  "Workflows",
@@ -109,6 +113,11 @@ const STAGENT_TOOLS: ToolCatalogEntry[] = [
109
113
  { name: "execute_workflow", description: "Start executing a workflow", group: "Workflows", paramHint: "workflowId" },
110
114
  { name: "delete_workflow", description: "Delete a workflow", group: "Workflows", paramHint: "workflowId" },
111
115
  { name: "get_workflow_status", description: "Get workflow execution progress", group: "Workflows", paramHint: "workflowId" },
116
+ { name: "list_blueprints", description: "List available workflow blueprints", group: "Workflows", paramHint: "domain, search" },
117
+ { name: "get_blueprint", description: "Get blueprint details and variables", group: "Workflows", paramHint: "blueprintId" },
118
+ { name: "instantiate_blueprint", description: "Create a workflow from a blueprint", group: "Workflows", paramHint: "blueprintId, variables" },
119
+ { name: "create_blueprint", description: "Create a custom workflow blueprint", group: "Workflows", paramHint: "yaml" },
120
+ { name: "delete_blueprint", description: "Delete a custom blueprint", group: "Workflows", paramHint: "blueprintId" },
112
121
 
113
122
  // ── Schedules ──
114
123
  { name: "list_schedules", description: "List scheduled prompt loops", group: "Schedules", paramHint: "status" },
@@ -133,6 +142,9 @@ const STAGENT_TOOLS: ToolCatalogEntry[] = [
133
142
  // ── Profiles ──
134
143
  { name: "list_profiles", description: "List available agent profiles", group: "Profiles" },
135
144
  { name: "get_profile", description: "Get agent profile configuration", group: "Profiles", paramHint: "profileId" },
145
+ { name: "create_profile", description: "Create a new agent profile", group: "Profiles", paramHint: "config, skillMd" },
146
+ { name: "update_profile", description: "Update a custom agent profile", group: "Profiles", paramHint: "profileId, config, skillMd" },
147
+ { name: "delete_profile", description: "Delete a custom agent profile", group: "Profiles", paramHint: "profileId" },
136
148
 
137
149
  // ── Usage ──
138
150
  { name: "get_usage_summary", description: "Get spending and token usage stats", group: "Usage", paramHint: "days" },
@@ -141,6 +153,12 @@ const STAGENT_TOOLS: ToolCatalogEntry[] = [
141
153
  { name: "get_settings", description: "Get current Stagent settings", group: "Settings", paramHint: "key" },
142
154
  { name: "set_settings", description: "Update a Stagent setting (approval required)", group: "Settings", paramHint: "key, value" },
143
155
 
156
+ // ── Skills ──
157
+ { name: "list_skills", description: "List all discoverable skills (user + project scopes)", group: "Skills" },
158
+ { name: "get_skill", description: "Get full SKILL.md content + metadata for one skill", group: "Skills", paramHint: "id" },
159
+ { name: "activate_skill", description: "Bind a skill to a conversation — SKILL.md is injected into every turn's system prompt. Pass mode='add' to compose (runtime-gated).", group: "Skills", paramHint: "conversationId, skillId, mode?" },
160
+ { name: "deactivate_skill", description: "Clear the active skill from a conversation", group: "Skills", paramHint: "conversationId" },
161
+
144
162
  // ── Tables ──
145
163
  { name: "list_tables", description: "List tables, filter by project or source", group: "Tables", paramHint: "projectId, source" },
146
164
  { name: "get_table_schema", description: "Get column definitions for a table", group: "Tables", paramHint: "tableId" },
@@ -188,6 +206,18 @@ const BROWSER_TOOLS: ToolCatalogEntry[] = [
188
206
  { name: "take_snapshot", description: "Take an accessibility snapshot", group: "Browser" },
189
207
  ];
190
208
 
209
+ const SESSION_ENTRIES: ToolCatalogEntry[] = [
210
+ { name: "clear", description: "Start a new conversation", group: "Session", behavior: "execute_immediately" },
211
+ { name: "compact", description: "Summarize and compact conversation history", group: "Session", behavior: "execute_immediately" },
212
+ { name: "export", description: "Save current conversation as a document", group: "Session", behavior: "execute_immediately" },
213
+ { name: "help", description: "Show chat shortcuts and commands", group: "Session", behavior: "execute_immediately" },
214
+ { name: "settings", description: "Open Stagent settings", group: "Session", behavior: "execute_immediately" },
215
+ { name: "new-task", description: "Create a new task", group: "Session", paramHint: "title" },
216
+ { name: "new-workflow", description: "Create a new workflow", group: "Session", paramHint: "name" },
217
+ { name: "new-schedule", description: "Create a new schedule", group: "Session", paramHint: "name, interval" },
218
+ { name: "new-from-template", description: "Start a conversation from a workflow blueprint", group: "Session", behavior: "execute_immediately" },
219
+ ];
220
+
191
221
  const UTILITY_ENTRIES: ToolCatalogEntry[] = [
192
222
  { name: "toggle_theme", description: "Switch dark/light mode", group: "Utility", behavior: "execute_immediately" },
193
223
  { name: "mark_all_read", description: "Mark all notifications as read", group: "Utility", behavior: "execute_immediately" },
@@ -203,13 +233,13 @@ export function getToolCatalog(opts?: { includeBrowser?: boolean }): ToolCatalog
203
233
 
204
234
  if (withBrowser) {
205
235
  if (!cachedWithBrowser) {
206
- cachedWithBrowser = [...STAGENT_TOOLS, ...BROWSER_TOOLS, ...UTILITY_ENTRIES];
236
+ cachedWithBrowser = [...SESSION_ENTRIES, ...STAGENT_TOOLS, ...BROWSER_TOOLS, ...UTILITY_ENTRIES];
207
237
  }
208
238
  return cachedWithBrowser;
209
239
  }
210
240
 
211
241
  if (!cachedCatalog) {
212
- cachedCatalog = [...STAGENT_TOOLS, ...UTILITY_ENTRIES];
242
+ cachedCatalog = [...SESSION_ENTRIES, ...STAGENT_TOOLS, ...UTILITY_ENTRIES];
213
243
  }
214
244
  return cachedCatalog;
215
245
  }
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it, vi, beforeEach } from "vitest";
2
+
3
+ vi.mock("@/lib/agents/profiles/list-fused-profiles", () => ({
4
+ listFusedProfiles: vi.fn(async (projectDir: string | null) =>
5
+ [
6
+ {
7
+ id: "general",
8
+ name: "General",
9
+ description: "Reg",
10
+ domain: "general",
11
+ tags: [],
12
+ },
13
+ projectDir
14
+ ? {
15
+ id: "project-only",
16
+ name: "Project Only",
17
+ description: "Proj",
18
+ domain: "skill",
19
+ tags: [],
20
+ origin: "filesystem-project",
21
+ }
22
+ : null,
23
+ ].filter(Boolean)
24
+ ),
25
+ }));
26
+
27
+ describe("list_profiles chat tool", () => {
28
+ beforeEach(() => {
29
+ vi.clearAllMocks();
30
+ });
31
+
32
+ it("returns fused profiles when called with a projectDir", async () => {
33
+ const { getListProfilesTool } = await import("@/lib/chat/tools/profile-tools");
34
+ const tool = getListProfilesTool("/fake/project");
35
+ const result = await tool.handler({});
36
+ // ok() wraps data as MCP content — parse the JSON text back out
37
+ const text = result.content[0].text;
38
+ const list = JSON.parse(text) as { id: string }[];
39
+ expect(Array.isArray(list)).toBe(true);
40
+ expect(list.some((p) => p.id === "project-only")).toBe(true);
41
+ });
42
+
43
+ it("returns registry-only profiles when projectDir is null", async () => {
44
+ const { getListProfilesTool } = await import("@/lib/chat/tools/profile-tools");
45
+ const tool = getListProfilesTool(null);
46
+ const result = await tool.handler({});
47
+ const text = result.content[0].text;
48
+ const list = JSON.parse(text) as { id: string }[];
49
+ expect(list.every((p) => p.id !== "project-only")).toBe(true);
50
+ });
51
+ });