stagent 0.1.10 → 0.1.12

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 (112) hide show
  1. package/README.md +58 -27
  2. package/package.json +3 -3
  3. package/src/__tests__/e2e/blueprint.test.ts +63 -0
  4. package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
  5. package/src/__tests__/e2e/helpers.ts +286 -0
  6. package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
  7. package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
  8. package/src/__tests__/e2e/setup.ts +156 -0
  9. package/src/__tests__/e2e/single-task.test.ts +170 -0
  10. package/src/app/api/command-palette/recent/route.ts +41 -18
  11. package/src/app/api/context/batch/route.ts +44 -0
  12. package/src/app/api/permissions/presets/route.ts +80 -0
  13. package/src/app/api/playbook/status/route.ts +15 -0
  14. package/src/app/api/profiles/route.ts +23 -21
  15. package/src/app/api/settings/pricing/route.ts +15 -0
  16. package/src/app/costs/page.tsx +53 -43
  17. package/src/app/globals.css +0 -5
  18. package/src/app/playbook/[slug]/page.tsx +76 -0
  19. package/src/app/playbook/page.tsx +54 -0
  20. package/src/app/profiles/page.tsx +7 -4
  21. package/src/app/settings/page.tsx +2 -2
  22. package/src/app/tasks/page.tsx +5 -0
  23. package/src/components/costs/cost-dashboard.tsx +226 -320
  24. package/src/components/dashboard/activity-feed.tsx +6 -2
  25. package/src/components/notifications/batch-proposal-review.tsx +150 -0
  26. package/src/components/notifications/notification-item.tsx +6 -3
  27. package/src/components/notifications/pending-approval-host.tsx +57 -11
  28. package/src/components/playbook/adoption-heatmap.tsx +69 -0
  29. package/src/components/playbook/journey-card.tsx +110 -0
  30. package/src/components/playbook/playbook-action-button.tsx +22 -0
  31. package/src/components/playbook/playbook-browser.tsx +143 -0
  32. package/src/components/playbook/playbook-card.tsx +102 -0
  33. package/src/components/playbook/playbook-detail-view.tsx +223 -0
  34. package/src/components/playbook/playbook-homepage.tsx +142 -0
  35. package/src/components/playbook/playbook-toc.tsx +90 -0
  36. package/src/components/playbook/playbook-updated-badge.tsx +23 -0
  37. package/src/components/playbook/related-docs.tsx +30 -0
  38. package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
  39. package/src/components/profiles/context-proposal-review.tsx +7 -3
  40. package/src/components/profiles/learned-context-panel.tsx +116 -8
  41. package/src/components/profiles/profile-detail-view.tsx +7 -19
  42. package/src/components/profiles/profile-form-view.tsx +0 -22
  43. package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
  44. package/src/components/settings/api-key-form.tsx +5 -43
  45. package/src/components/settings/auth-config-section.tsx +10 -6
  46. package/src/components/settings/auth-status-badge.tsx +8 -0
  47. package/src/components/settings/budget-guardrails-section.tsx +403 -620
  48. package/src/components/settings/connection-test-control.tsx +63 -0
  49. package/src/components/settings/permissions-section.tsx +85 -75
  50. package/src/components/settings/permissions-sections.tsx +24 -0
  51. package/src/components/settings/presets-section.tsx +159 -0
  52. package/src/components/settings/pricing-registry-panel.tsx +164 -0
  53. package/src/components/shared/app-sidebar.tsx +2 -0
  54. package/src/components/shared/command-palette.tsx +30 -0
  55. package/src/components/shared/light-markdown.tsx +134 -0
  56. package/src/components/workflows/loop-status-view.tsx +8 -4
  57. package/src/components/workflows/workflow-status-view.tsx +16 -9
  58. package/src/lib/agents/__tests__/claude-agent.test.ts +7 -2
  59. package/src/lib/agents/__tests__/learned-context.test.ts +500 -0
  60. package/src/lib/agents/__tests__/pattern-extractor.test.ts +243 -0
  61. package/src/lib/agents/__tests__/sweep.test.ts +202 -0
  62. package/src/lib/agents/claude-agent.ts +104 -78
  63. package/src/lib/agents/learned-context.ts +32 -28
  64. package/src/lib/agents/learning-session.ts +234 -0
  65. package/src/lib/agents/pattern-extractor.ts +34 -64
  66. package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
  67. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +0 -1
  68. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +0 -1
  69. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +0 -1
  70. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +0 -1
  71. package/src/lib/agents/profiles/builtins/general/profile.yaml +0 -1
  72. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +0 -1
  73. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +0 -1
  74. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +0 -1
  75. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +0 -1
  76. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +0 -1
  77. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +0 -1
  78. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +0 -1
  79. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +0 -1
  80. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +0 -1
  81. package/src/lib/agents/profiles/registry.ts +0 -1
  82. package/src/lib/agents/profiles/sort.ts +7 -0
  83. package/src/lib/agents/profiles/types.ts +0 -1
  84. package/src/lib/agents/runtime/catalog.ts +1 -1
  85. package/src/lib/agents/runtime/claude.ts +66 -0
  86. package/src/lib/constants/settings.ts +1 -0
  87. package/src/lib/constants/task-status.ts +6 -0
  88. package/src/lib/data/seed-data/profiles.ts +0 -3
  89. package/src/lib/db/schema.ts +3 -0
  90. package/src/lib/docs/adoption.ts +105 -0
  91. package/src/lib/docs/journey-tracker.ts +21 -0
  92. package/src/lib/docs/reader.ts +102 -0
  93. package/src/lib/docs/types.ts +54 -0
  94. package/src/lib/docs/usage-stage.ts +60 -0
  95. package/src/lib/notifications/actionable.ts +18 -10
  96. package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
  97. package/src/lib/settings/budget-guardrails.ts +213 -85
  98. package/src/lib/settings/permission-presets.ts +150 -0
  99. package/src/lib/settings/runtime-setup.ts +71 -0
  100. package/src/lib/usage/__tests__/ledger.test.ts +29 -5
  101. package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
  102. package/src/lib/usage/ledger.ts +4 -2
  103. package/src/lib/usage/pricing-registry.ts +570 -0
  104. package/src/lib/usage/pricing.ts +15 -41
  105. package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
  106. package/src/lib/utils/learned-context-history.ts +150 -0
  107. package/src/lib/validators/__tests__/profile.test.ts +0 -15
  108. package/src/lib/validators/__tests__/settings.test.ts +23 -16
  109. package/src/lib/validators/profile.ts +0 -1
  110. package/src/lib/validators/settings.ts +3 -9
  111. package/src/lib/workflows/__tests__/engine.test.ts +2 -0
  112. package/src/lib/workflows/engine.ts +20 -1
@@ -2,7 +2,7 @@ import { db } from "@/lib/db";
2
2
  import { learnedContext, notifications } from "@/lib/db/schema";
3
3
  import { and, desc, eq } from "drizzle-orm";
4
4
  import type { LearnedContextRow } from "@/lib/db/schema";
5
- import Anthropic from "@anthropic-ai/sdk";
5
+ import { runMetaCompletion } from "./runtime/claude";
6
6
 
7
7
  const CONTEXT_CHAR_LIMIT = 8_000;
8
8
  const SUMMARIZATION_THRESHOLD = 6_000;
@@ -58,14 +58,24 @@ function getNextVersion(profileId: string): number {
58
58
  // Proposal flow
59
59
  // ---------------------------------------------------------------------------
60
60
 
61
- /** Insert a context proposal and create a notification for human review */
61
+ /**
62
+ * Insert a context proposal and optionally create a notification for human review.
63
+ *
64
+ * When `options.silent` is true, the proposal row is created but no notification
65
+ * is generated. This is used by the learning session system to buffer proposals
66
+ * during workflow execution — a batch notification is created when the session closes.
67
+ *
68
+ * Returns the learned_context row ID (not the notification ID) so callers can
69
+ * reference the proposal regardless of whether a notification was created.
70
+ */
62
71
  export async function proposeContextAddition(
63
72
  profileId: string,
64
73
  taskId: string,
65
- additions: string
74
+ additions: string,
75
+ options?: { silent?: boolean }
66
76
  ): Promise<string> {
67
77
  const version = getNextVersion(profileId);
68
- const notificationId = crypto.randomUUID();
78
+ const notificationId = options?.silent ? null : crypto.randomUUID();
69
79
  const rowId = crypto.randomUUID();
70
80
  const now = new Date();
71
81
 
@@ -83,19 +93,21 @@ export async function proposeContextAddition(
83
93
  createdAt: now,
84
94
  });
85
95
 
86
- // Create notification for human review
87
- await db.insert(notifications).values({
88
- id: notificationId,
89
- taskId,
90
- type: "context_proposal",
91
- title: `Context proposal for ${profileId}`,
92
- body: additions.slice(0, 500),
93
- toolName: profileId,
94
- toolInput: JSON.stringify({ profileId, additions, learnedContextId: rowId }),
95
- createdAt: now,
96
- });
96
+ // Create notification for human review (unless silent)
97
+ if (notificationId) {
98
+ await db.insert(notifications).values({
99
+ id: notificationId,
100
+ taskId,
101
+ type: "context_proposal",
102
+ title: `Context proposal for ${profileId}`,
103
+ body: additions.slice(0, 500),
104
+ toolName: profileId,
105
+ toolInput: JSON.stringify({ profileId, additions, learnedContextId: rowId }),
106
+ createdAt: now,
107
+ });
108
+ }
97
109
 
98
- return notificationId;
110
+ return rowId;
99
111
  }
100
112
 
101
113
  // ---------------------------------------------------------------------------
@@ -243,14 +255,8 @@ export async function summarizeContext(profileId: string): Promise<void> {
243
255
  const content = getActiveLearnedContext(profileId);
244
256
  if (!content || content.length <= SUMMARIZATION_THRESHOLD) return;
245
257
 
246
- const client = new Anthropic();
247
- const response = await client.messages.create({
248
- model: "claude-sonnet-4-20250514",
249
- max_tokens: 2048,
250
- messages: [
251
- {
252
- role: "user",
253
- content: `You are condensing learned context for an AI agent profile "${profileId}".
258
+ const { text } = await runMetaCompletion({
259
+ prompt: `You are condensing learned context for an AI agent profile "${profileId}".
254
260
  The current context has grown to ${content.length} characters and needs to be summarized to under ${SUMMARIZATION_THRESHOLD} characters while preserving all key patterns, best practices, and important insights.
255
261
 
256
262
  Current learned context:
@@ -266,12 +272,10 @@ Produce a condensed version that:
266
272
  5. Stays under ${SUMMARIZATION_THRESHOLD} characters
267
273
 
268
274
  Output ONLY the condensed context, no preamble.`,
269
- },
270
- ],
275
+ activityType: "context_summarization",
271
276
  });
272
277
 
273
- const summarized =
274
- response.content[0].type === "text" ? response.content[0].text : "";
278
+ const summarized = text.trim();
275
279
 
276
280
  if (!summarized || summarized.length >= content.length) return;
277
281
 
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Learning Session — buffers context proposals during workflow execution.
3
+ *
4
+ * When a workflow starts, a session is opened. All context proposals generated
5
+ * by tasks within that workflow are buffered instead of creating individual
6
+ * notifications. When the workflow completes (or fails), the session is closed
7
+ * and a single batch notification is created for all buffered proposals.
8
+ */
9
+
10
+ import { db } from "@/lib/db";
11
+ import { learnedContext, notifications, tasks } from "@/lib/db/schema";
12
+ import { eq, and } from "drizzle-orm";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // In-memory session registry
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /**
19
+ * Active learning sessions, keyed by workflowId.
20
+ * Each session accumulates proposal IDs (learnedContext row IDs) until closed.
21
+ */
22
+ const activeSessions = new Map<
23
+ string,
24
+ {
25
+ workflowId: string;
26
+ proposalIds: string[];
27
+ openedAt: Date;
28
+ }
29
+ >();
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Session lifecycle
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /**
36
+ * Open a learning session for a workflow. All proposals created during this
37
+ * session will be buffered instead of generating individual notifications.
38
+ */
39
+ export function openLearningSession(workflowId: string): void {
40
+ activeSessions.set(workflowId, {
41
+ workflowId,
42
+ proposalIds: [],
43
+ openedAt: new Date(),
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Check if a learning session is active for a workflow.
49
+ */
50
+ export function hasLearningSession(workflowId: string): boolean {
51
+ return activeSessions.has(workflowId);
52
+ }
53
+
54
+ /**
55
+ * Get the active session for a workflow (or null if none).
56
+ */
57
+ export function getLearningSession(workflowId: string) {
58
+ return activeSessions.get(workflowId) ?? null;
59
+ }
60
+
61
+ /**
62
+ * Buffer a proposal ID into the active session.
63
+ * Called by proposeContextAddition when it detects a workflow session.
64
+ */
65
+ export function bufferProposal(
66
+ workflowId: string,
67
+ proposalRowId: string
68
+ ): void {
69
+ const session = activeSessions.get(workflowId);
70
+ if (session) {
71
+ session.proposalIds.push(proposalRowId);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Close the learning session and create a single batch notification
77
+ * for all buffered proposals. Returns the batch notification ID,
78
+ * or null if there were no proposals.
79
+ */
80
+ export async function closeLearningSession(
81
+ workflowId: string
82
+ ): Promise<string | null> {
83
+ const session = activeSessions.get(workflowId);
84
+ activeSessions.delete(workflowId);
85
+
86
+ if (!session || session.proposalIds.length === 0) {
87
+ return null;
88
+ }
89
+
90
+ // Load all buffered proposal rows
91
+ const proposals = [];
92
+ for (const id of session.proposalIds) {
93
+ const [row] = db
94
+ .select()
95
+ .from(learnedContext)
96
+ .where(eq(learnedContext.id, id))
97
+ .all();
98
+ if (row) {
99
+ proposals.push(row);
100
+ }
101
+ }
102
+
103
+ if (proposals.length === 0) return null;
104
+
105
+ // Group by profile for the notification body
106
+ const byProfile = new Map<string, typeof proposals>();
107
+ for (const p of proposals) {
108
+ const group = byProfile.get(p.profileId) ?? [];
109
+ group.push(p);
110
+ byProfile.set(p.profileId, group);
111
+ }
112
+
113
+ // Build summary body
114
+ const bodyLines: string[] = [];
115
+ for (const [profileId, group] of byProfile) {
116
+ bodyLines.push(`**${profileId}** (${group.length} proposal${group.length > 1 ? "s" : ""}):`);
117
+ for (const p of group) {
118
+ const preview = (p.proposedAdditions ?? p.diff ?? "").slice(0, 150);
119
+ bodyLines.push(` - ${preview}${preview.length >= 150 ? "..." : ""}`);
120
+ }
121
+ }
122
+
123
+ const notificationId = crypto.randomUUID();
124
+
125
+ await db.insert(notifications).values({
126
+ id: notificationId,
127
+ taskId: null,
128
+ type: "context_proposal_batch",
129
+ title: `Workflow learning: ${proposals.length} context proposal${proposals.length > 1 ? "s" : ""}`,
130
+ body: bodyLines.join("\n").slice(0, 1000),
131
+ toolName: "workflow-context-batch",
132
+ toolInput: JSON.stringify({
133
+ workflowId,
134
+ proposalIds: session.proposalIds,
135
+ profileIds: [...byProfile.keys()],
136
+ }),
137
+ createdAt: new Date(),
138
+ });
139
+
140
+ return notificationId;
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Task → Workflow mapping
145
+ // ---------------------------------------------------------------------------
146
+
147
+ /**
148
+ * Resolve the workflowId for a given task. Returns null if the task is
149
+ * not part of a workflow or the task doesn't exist.
150
+ */
151
+ export function getTaskWorkflowId(taskId: string): string | null {
152
+ try {
153
+ const [row] = db
154
+ .select({ workflowId: tasks.workflowId })
155
+ .from(tasks)
156
+ .where(eq(tasks.id, taskId))
157
+ .all();
158
+
159
+ return row?.workflowId ?? null;
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Batch operations
167
+ // ---------------------------------------------------------------------------
168
+
169
+ /**
170
+ * Approve all proposals in a batch by their IDs.
171
+ */
172
+ export async function batchApproveProposals(
173
+ proposalRowIds: string[]
174
+ ): Promise<number> {
175
+ // Import inline to avoid circular dependency
176
+ const { approveProposal } = await import("./learned-context");
177
+
178
+ let approved = 0;
179
+ for (const rowId of proposalRowIds) {
180
+ const [row] = db
181
+ .select()
182
+ .from(learnedContext)
183
+ .where(
184
+ and(
185
+ eq(learnedContext.id, rowId),
186
+ eq(learnedContext.changeType, "proposal")
187
+ )
188
+ )
189
+ .all();
190
+
191
+ if (row?.proposalNotificationId) {
192
+ try {
193
+ await approveProposal(row.proposalNotificationId);
194
+ approved++;
195
+ } catch {
196
+ // Skip if already approved/rejected
197
+ }
198
+ }
199
+ }
200
+ return approved;
201
+ }
202
+
203
+ /**
204
+ * Reject all proposals in a batch by their IDs.
205
+ */
206
+ export async function batchRejectProposals(
207
+ proposalRowIds: string[]
208
+ ): Promise<number> {
209
+ const { rejectProposal } = await import("./learned-context");
210
+
211
+ let rejected = 0;
212
+ for (const rowId of proposalRowIds) {
213
+ const [row] = db
214
+ .select()
215
+ .from(learnedContext)
216
+ .where(
217
+ and(
218
+ eq(learnedContext.id, rowId),
219
+ eq(learnedContext.changeType, "proposal")
220
+ )
221
+ )
222
+ .all();
223
+
224
+ if (row?.proposalNotificationId) {
225
+ try {
226
+ await rejectProposal(row.proposalNotificationId);
227
+ rejected++;
228
+ } catch {
229
+ // Skip if already approved/rejected
230
+ }
231
+ }
232
+ }
233
+ return rejected;
234
+ }
@@ -1,4 +1,3 @@
1
- import Anthropic from "@anthropic-ai/sdk";
2
1
  import { db } from "@/lib/db";
3
2
  import { tasks, agentLogs } from "@/lib/db/schema";
4
3
  import { eq, desc } from "drizzle-orm";
@@ -6,6 +5,12 @@ import {
6
5
  getActiveLearnedContext,
7
6
  proposeContextAddition,
8
7
  } from "./learned-context";
8
+ import { runMetaCompletion } from "./runtime/claude";
9
+ import {
10
+ getTaskWorkflowId,
11
+ hasLearningSession,
12
+ bufferProposal,
13
+ } from "./learning-session";
9
14
 
10
15
  export interface PatternEntry {
11
16
  title: string;
@@ -17,50 +22,9 @@ export interface PatternProposal {
17
22
  patterns: PatternEntry[];
18
23
  }
19
24
 
20
- const PATTERN_TOOL: Anthropic.Messages.Tool = {
21
- name: "propose_learned_patterns",
22
- description:
23
- "Propose patterns learned from this task execution that should be remembered for future tasks with this profile.",
24
- input_schema: {
25
- type: "object" as const,
26
- properties: {
27
- patterns: {
28
- type: "array",
29
- items: {
30
- type: "object",
31
- properties: {
32
- title: {
33
- type: "string",
34
- description: "Short pattern name (2-6 words)",
35
- },
36
- description: {
37
- type: "string",
38
- description:
39
- "Concise description of the pattern or lesson (1-2 sentences)",
40
- },
41
- category: {
42
- type: "string",
43
- enum: [
44
- "error_resolution",
45
- "best_practice",
46
- "shortcut",
47
- "preference",
48
- ],
49
- },
50
- },
51
- required: ["title", "description", "category"],
52
- },
53
- description:
54
- "Patterns worth remembering. Return empty array if nothing notable.",
55
- },
56
- },
57
- required: ["patterns"],
58
- },
59
- };
60
-
61
25
  /**
62
26
  * Analyze a completed task for patterns worth learning.
63
- * Makes a focused Claude API call, then proposes additions if patterns found.
27
+ * Routes through the Claude Agent SDK runtime (no direct Anthropic SDK usage).
64
28
  * Returns the notification ID if a proposal was created, null otherwise.
65
29
  */
66
30
  export async function analyzeForLearnedPatterns(
@@ -99,16 +63,13 @@ export async function analyzeForLearnedPatterns(
99
63
  })
100
64
  .join("\n");
101
65
 
102
- const client = new Anthropic();
103
- const response = await client.messages.create({
104
- model: "claude-sonnet-4-20250514",
105
- max_tokens: 1024,
106
- tools: [PATTERN_TOOL],
107
- tool_choice: { type: "tool", name: "propose_learned_patterns" },
108
- messages: [
109
- {
110
- role: "user",
111
- content: `Analyze this completed task for patterns worth learning for the "${profileId}" agent profile.
66
+ const { text } = await runMetaCompletion({
67
+ prompt: `Analyze this completed task for patterns worth learning for the "${profileId}" agent profile.
68
+
69
+ Return ONLY a JSON array (no markdown, no code fences):
70
+ [{"title": "...", "description": "...", "category": "error_resolution|best_practice|shortcut|preference"}]
71
+
72
+ Return an empty array [] if no noteworthy patterns.
112
73
 
113
74
  ## Task
114
75
  Title: ${task.title}
@@ -124,27 +85,36 @@ ${logSummary.slice(0, 2000)}
124
85
  ${currentContext ?? "(none yet)"}
125
86
 
126
87
  Extract ONLY genuinely useful patterns — things that would help this profile avoid mistakes or work more efficiently on similar future tasks. If this task was routine with nothing notable, return an empty patterns array. Do NOT repeat patterns already in the learned context.`,
127
- },
128
- ],
88
+ activityType: "pattern_extraction",
129
89
  });
130
90
 
131
- // Extract the tool use result
132
- const toolBlock = response.content.find(
133
- (block) => block.type === "tool_use" && block.name === "propose_learned_patterns"
134
- );
135
-
136
- if (!toolBlock || toolBlock.type !== "tool_use") return null;
91
+ // Parse JSON array from response text
92
+ const jsonMatch = text.match(/\[[\s\S]*\]/);
93
+ const patterns: PatternEntry[] = jsonMatch ? JSON.parse(jsonMatch[0]) : [];
137
94
 
138
- const proposal = toolBlock.input as PatternProposal;
139
- if (!proposal.patterns || proposal.patterns.length === 0) return null;
95
+ if (patterns.length === 0) return null;
140
96
 
141
97
  // Format patterns as text for the proposal
142
- const formattedAdditions = proposal.patterns
98
+ const formattedAdditions = patterns
143
99
  .map(
144
100
  (p) =>
145
101
  `### ${p.title} [${p.category}]\n${p.description}`
146
102
  )
147
103
  .join("\n\n");
148
104
 
105
+ // Check if this task is part of a workflow with an active learning session.
106
+ // If so, buffer the proposal instead of creating an individual notification.
107
+ const workflowId = getTaskWorkflowId(taskId);
108
+ if (workflowId && hasLearningSession(workflowId)) {
109
+ const rowId = await proposeContextAddition(
110
+ profileId,
111
+ taskId,
112
+ formattedAdditions,
113
+ { silent: true }
114
+ );
115
+ bufferProposal(workflowId, rowId);
116
+ return rowId;
117
+ }
118
+
149
119
  return proposeContextAddition(profileId, taskId, formattedAdditions);
150
120
  }
@@ -0,0 +1,42 @@
1
+ import type { AgentProfile } from "../types";
2
+
3
+ import { sortProfilesByName } from "../sort";
4
+
5
+ function makeProfile(id: string, name: string): AgentProfile {
6
+ return {
7
+ id,
8
+ name,
9
+ description: `${name} description`,
10
+ domain: "work",
11
+ tags: [],
12
+ systemPrompt: "",
13
+ skillMd: "",
14
+ allowedTools: [],
15
+ mcpServers: {},
16
+ canUseToolPolicy: false,
17
+ maxTurns: 20,
18
+ outputFormat: "text",
19
+ version: "1.0.0",
20
+ author: "test",
21
+ source: "test",
22
+ tests: [],
23
+ supportedRuntimes: ["claude-code"],
24
+ runtimeOverrides: {},
25
+ };
26
+ }
27
+
28
+ describe("sortProfilesByName", () => {
29
+ it("sorts profiles alphabetically so sweep remains discoverable", () => {
30
+ const sorted = sortProfilesByName([
31
+ makeProfile("sweep", "Sweep"),
32
+ makeProfile("general", "General"),
33
+ makeProfile("api-tester", "API Tester"),
34
+ ]);
35
+
36
+ expect(sorted.map((profile) => profile.id)).toEqual([
37
+ "api-tester",
38
+ "general",
39
+ "sweep",
40
+ ]);
41
+ });
42
+ });
@@ -15,7 +15,6 @@ canUseToolPolicy:
15
15
  autoApprove: [Read, Grep, Glob]
16
16
  autoDeny: []
17
17
 
18
- temperature: 0.3
19
18
  maxTurns: 20
20
19
  outputFormat: structured-findings
21
20
 
@@ -15,7 +15,6 @@ canUseToolPolicy:
15
15
  autoApprove: [Read, Grep, Glob]
16
16
  autoDeny: []
17
17
 
18
- temperature: 0.3
19
18
  maxTurns: 30
20
19
 
21
20
  author: stagent
@@ -15,7 +15,6 @@ canUseToolPolicy:
15
15
  autoApprove: [Read, Grep, Glob]
16
16
  autoDeny: []
17
17
 
18
- temperature: 0.3
19
18
  maxTurns: 30
20
19
 
21
20
  author: stagent
@@ -14,7 +14,6 @@ canUseToolPolicy:
14
14
  autoApprove: [Read]
15
15
  autoDeny: []
16
16
 
17
- temperature: 0.5
18
17
  maxTurns: 20
19
18
  outputFormat: markdown-document
20
19
 
@@ -12,7 +12,6 @@ runtimeOverrides:
12
12
  Stay pragmatic, execute the requested work directly, and prefer concise operational updates.
13
13
  Keep outputs grounded in the current workspace and call out blocked actions explicitly.
14
14
 
15
- temperature: 0.5
16
15
  maxTurns: 30
17
16
 
18
17
  author: stagent
@@ -14,7 +14,6 @@ canUseToolPolicy:
14
14
  autoApprove: [WebSearch, WebFetch, Read]
15
15
  autoDeny: [Bash, Write, Edit]
16
16
 
17
- temperature: 0.6
18
17
  maxTurns: 20
19
18
 
20
19
  author: stagent
@@ -14,7 +14,6 @@ canUseToolPolicy:
14
14
  autoApprove: [WebSearch, WebFetch, Read]
15
15
  autoDeny: [Bash, Write, Edit]
16
16
 
17
- temperature: 0.5
18
17
  maxTurns: 25
19
18
 
20
19
  author: stagent
@@ -14,7 +14,6 @@ canUseToolPolicy:
14
14
  autoApprove: [Read, Grep, Glob]
15
15
  autoDeny: []
16
16
 
17
- temperature: 0.4
18
17
  maxTurns: 25
19
18
 
20
19
  author: stagent
@@ -15,7 +15,6 @@ canUseToolPolicy:
15
15
  autoApprove: [WebSearch, WebFetch, Read]
16
16
  autoDeny: []
17
17
 
18
- temperature: 0.4
19
18
  maxTurns: 25
20
19
 
21
20
  author: stagent
@@ -14,7 +14,6 @@ canUseToolPolicy:
14
14
  autoApprove: [WebSearch, WebFetch, Read]
15
15
  autoDeny: [Bash, Write, Edit]
16
16
 
17
- temperature: 0.5
18
17
  maxTurns: 20
19
18
 
20
19
  author: stagent
@@ -5,7 +5,6 @@ domain: work
5
5
  tags: [sweep, audit, improvement, maintenance]
6
6
  supportedRuntimes: [claude-code]
7
7
 
8
- temperature: 0.3
9
8
  maxTurns: 50
10
9
  outputFormat: json
11
10
 
@@ -16,7 +16,6 @@ canUseToolPolicy:
16
16
  autoApprove: [Read, Grep, Glob]
17
17
  autoDeny: []
18
18
 
19
- temperature: 0.4
20
19
  maxTurns: 20
21
20
  outputFormat: markdown
22
21
 
@@ -14,7 +14,6 @@ canUseToolPolicy:
14
14
  autoApprove: [WebSearch, WebFetch, Read]
15
15
  autoDeny: [Bash, Write, Edit]
16
16
 
17
- temperature: 0.6
18
17
  maxTurns: 25
19
18
 
20
19
  author: stagent
@@ -14,7 +14,6 @@ canUseToolPolicy:
14
14
  autoApprove: [Read]
15
15
  autoDeny: [Bash, Write, Edit]
16
16
 
17
- temperature: 0.3
18
17
  maxTurns: 20
19
18
 
20
19
  author: stagent
@@ -174,7 +174,6 @@ function scanProfiles(): Map<string, AgentProfile> {
174
174
  allowedTools: config.allowedTools,
175
175
  mcpServers: config.mcpServers as Record<string, unknown>,
176
176
  canUseToolPolicy: config.canUseToolPolicy,
177
- temperature: config.temperature,
178
177
  maxTurns: config.maxTurns,
179
178
  outputFormat: config.outputFormat,
180
179
  version: config.version,
@@ -0,0 +1,7 @@
1
+ export function sortProfilesByName<T extends { name: string }>(
2
+ profiles: T[]
3
+ ): T[] {
4
+ return [...profiles].sort((a, b) =>
5
+ a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
6
+ );
7
+ }
@@ -31,7 +31,6 @@ export interface AgentProfile {
31
31
  allowedTools?: string[];
32
32
  mcpServers?: Record<string, unknown>;
33
33
  canUseToolPolicy?: CanUseToolPolicy;
34
- temperature?: number;
35
34
  maxTurns?: number;
36
35
  outputFormat?: string;
37
36
  version?: string;
@@ -50,7 +50,7 @@ const RUNTIME_CATALOG: Record<AgentRuntimeId, RuntimeCatalogEntry> = {
50
50
  resume: true,
51
51
  cancel: true,
52
52
  approvals: true,
53
- mcpServers: true,
53
+ mcpServers: false, // Not yet wired — configs not passed to codex subprocess
54
54
  profileTests: false,
55
55
  taskAssist: true,
56
56
  authHealthCheck: true,