@desplega.ai/agent-swarm 1.71.2 → 1.72.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.
- package/README.md +3 -2
- package/openapi.json +994 -62
- package/package.json +2 -1
- package/src/be/budget-admission.ts +121 -0
- package/src/be/budget-refusal-notify.ts +145 -0
- package/src/be/db.ts +488 -5
- package/src/be/migrations/044_provider_meta.sql +2 -0
- package/src/be/migrations/046_budgets_and_pricing.sql +87 -0
- package/src/be/migrations/047_session_costs_cost_source.sql +16 -0
- package/src/cli.tsx +22 -1
- package/src/commands/claude-managed-setup.ts +687 -0
- package/src/commands/codex-login.ts +1 -1
- package/src/commands/runner.ts +175 -28
- package/src/commands/templates.ts +10 -6
- package/src/http/budgets.ts +219 -0
- package/src/http/index.ts +6 -0
- package/src/http/integrations.ts +134 -0
- package/src/http/poll.ts +161 -3
- package/src/http/pricing.ts +245 -0
- package/src/http/session-data.ts +54 -6
- package/src/http/tasks.ts +23 -2
- package/src/prompts/base-prompt.ts +103 -73
- package/src/prompts/session-templates.ts +43 -0
- package/src/providers/claude-adapter.ts +3 -1
- package/src/providers/claude-managed-adapter.ts +871 -0
- package/src/providers/claude-managed-models.ts +117 -0
- package/src/providers/claude-managed-swarm-events.ts +77 -0
- package/src/providers/codex-adapter.ts +3 -1
- package/src/providers/codex-skill-resolver.ts +10 -0
- package/src/providers/codex-swarm-events.ts +20 -161
- package/src/providers/devin-adapter.ts +894 -0
- package/src/providers/devin-api.ts +207 -0
- package/src/providers/devin-playbooks.ts +91 -0
- package/src/providers/devin-skill-resolver.ts +113 -0
- package/src/providers/index.ts +10 -1
- package/src/providers/pi-mono-adapter.ts +3 -1
- package/src/providers/swarm-events-shared.ts +262 -0
- package/src/providers/types.ts +26 -1
- package/src/tests/base-prompt.test.ts +199 -0
- package/src/tests/budget-admission.test.ts +339 -0
- package/src/tests/budget-claim-gate.test.ts +288 -0
- package/src/tests/budget-refusal-notification.test.ts +324 -0
- package/src/tests/budgets-routes.test.ts +331 -0
- package/src/tests/claude-managed-adapter.test.ts +1301 -0
- package/src/tests/claude-managed-setup.test.ts +325 -0
- package/src/tests/devin-adapter.test.ts +677 -0
- package/src/tests/devin-api.test.ts +339 -0
- package/src/tests/integrations-http.test.ts +211 -0
- package/src/tests/migration-046-budgets.test.ts +327 -0
- package/src/tests/pricing-routes.test.ts +315 -0
- package/src/tests/prompt-template-remaining.test.ts +4 -0
- package/src/tests/prompt-template-session.test.ts +2 -2
- package/src/tests/provider-adapter.test.ts +1 -1
- package/src/tests/runner-budget-refused.test.ts +271 -0
- package/src/tests/session-costs-codex-recompute.test.ts +386 -0
- package/src/tools/poll-task.ts +13 -2
- package/src/tools/task-action.ts +92 -2
- package/src/tools/templates.ts +29 -0
- package/src/types.ts +116 -0
- package/src/utils/budget-backoff.ts +34 -0
- package/src/utils/credentials.ts +4 -0
- package/src/utils/provider-metadata.ts +9 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared adapter-side swarm-event handler factory.
|
|
3
|
+
*
|
|
4
|
+
* Phase 5 extraction (see thoughts/.../2026-04-28-claude-managed-agents-provider.md
|
|
5
|
+
* § Phase 5). Originally lived inline in `codex-swarm-events.ts`; lifted out
|
|
6
|
+
* here so the `claude-managed` adapter (and any future adapter that wants the
|
|
7
|
+
* same throttle/poll/heartbeat scaffolding) can reuse it.
|
|
8
|
+
*
|
|
9
|
+
* ## What this owns
|
|
10
|
+
*
|
|
11
|
+
* - `apiHeaders`, `fireAndForget`
|
|
12
|
+
* - Throttle constants and the `shouldRun` gate
|
|
13
|
+
* - `checkCancelled`, `checkLoop`, `heartbeat`, `activity`
|
|
14
|
+
* - `progressContextUsage`, `progressCompaction`, `progressCompletion`
|
|
15
|
+
*
|
|
16
|
+
* ## What this does NOT own
|
|
17
|
+
*
|
|
18
|
+
* Provider-specific dispatch lives in each adapter's `*-swarm-events.ts`. For
|
|
19
|
+
* example codex's per-turn cost-data shape, or claude-managed's interrupt+
|
|
20
|
+
* archive cancel callback. The shared file exposes the throttled primitives
|
|
21
|
+
* and a generic event-dispatch shell; the per-provider wrapper supplies any
|
|
22
|
+
* `onCancel` / extension points it needs.
|
|
23
|
+
*
|
|
24
|
+
* ## Two-layer cancellation (unchanged from codex)
|
|
25
|
+
*
|
|
26
|
+
* Layer 1 — runner-side polling: `src/commands/runner.ts` polls
|
|
27
|
+
* `GET /cancelled-tasks` on a timer and calls `session.abort()`. All providers
|
|
28
|
+
* inherit this for free.
|
|
29
|
+
*
|
|
30
|
+
* Layer 2 — adapter-side (this file): on every `tool_start` we (throttled)
|
|
31
|
+
* check the same endpoint and abort the running turn via the shared
|
|
32
|
+
* `AbortController`. This *accelerates* cancellation latency.
|
|
33
|
+
*
|
|
34
|
+
* ## Hard contract
|
|
35
|
+
*
|
|
36
|
+
* - The handler is synchronous from the caller's perspective.
|
|
37
|
+
* - Every fetch is fire-and-forget with `.catch(() => {})` so a single bad
|
|
38
|
+
* request never breaks the session.
|
|
39
|
+
* - The handler never throws — `try/catch` around the dispatch swallows
|
|
40
|
+
* everything for safety.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { checkToolLoop } from "../hooks/tool-loop-detection";
|
|
44
|
+
import type { ProviderEvent } from "./types";
|
|
45
|
+
|
|
46
|
+
/** Throttle windows (ms) keyed by action name. Exported for unit assertions. */
|
|
47
|
+
export const CANCELLATION_THROTTLE_MS = 500;
|
|
48
|
+
export const HEARTBEAT_THROTTLE_MS = 5_000;
|
|
49
|
+
export const ACTIVITY_THROTTLE_MS = 5_000;
|
|
50
|
+
export const CONTEXT_THROTTLE_MS = 30_000;
|
|
51
|
+
|
|
52
|
+
export interface SwarmEventHandlerOpts {
|
|
53
|
+
apiUrl: string;
|
|
54
|
+
apiKey: string;
|
|
55
|
+
agentId: string;
|
|
56
|
+
/** Task currently being worked on. When null, all task-scoped hooks are no-ops. */
|
|
57
|
+
taskId: string | null;
|
|
58
|
+
/** Mutable reference to the session's per-turn AbortController. */
|
|
59
|
+
abortRef: { current: AbortController | null };
|
|
60
|
+
/**
|
|
61
|
+
* Optional callback invoked when a cancellation is detected (in addition to
|
|
62
|
+
* the abort-controller fire). Adapters that need provider-specific cancel
|
|
63
|
+
* actions (e.g. claude-managed sending `user.interrupt` + archiving) wire
|
|
64
|
+
* it here. Errors are swallowed.
|
|
65
|
+
*/
|
|
66
|
+
onCancel?: () => void | Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Prefix used in the synthetic `sessionId` body field on context POSTs when
|
|
69
|
+
* `session_init` hasn't fired yet. Codex preserves its historical
|
|
70
|
+
* `codex-${taskId}` shape; new providers can pick their own.
|
|
71
|
+
*/
|
|
72
|
+
sessionIdFallbackPrefix?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function apiHeaders(opts: SwarmEventHandlerOpts): Record<string, string> {
|
|
76
|
+
return {
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
79
|
+
"X-Agent-ID": opts.agentId,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function fireAndForget(url: string, init: RequestInit): void {
|
|
84
|
+
void fetch(url, init).catch(() => {});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Build the handler. The returned function reacts to normalized events.
|
|
89
|
+
*
|
|
90
|
+
* Mirrors the codex-swarm-events behavior exactly — providers that want
|
|
91
|
+
* additional event-type handling can compose this with their own dispatch
|
|
92
|
+
* (e.g. by wrapping the returned handler).
|
|
93
|
+
*/
|
|
94
|
+
export function createSwarmEventHandler(
|
|
95
|
+
opts: SwarmEventHandlerOpts,
|
|
96
|
+
): (event: ProviderEvent) => void {
|
|
97
|
+
const lastCall: Record<string, number> = {};
|
|
98
|
+
let sessionId: string | undefined;
|
|
99
|
+
|
|
100
|
+
const shouldRun = (key: string, throttleMs: number): boolean => {
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
if (now - (lastCall[key] ?? 0) < throttleMs) return false;
|
|
103
|
+
lastCall[key] = now;
|
|
104
|
+
return true;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const checkCancelled = (): void => {
|
|
108
|
+
const taskId = opts.taskId;
|
|
109
|
+
if (!taskId) return;
|
|
110
|
+
void (async () => {
|
|
111
|
+
try {
|
|
112
|
+
const res = await fetch(
|
|
113
|
+
`${opts.apiUrl}/cancelled-tasks?taskId=${encodeURIComponent(taskId)}`,
|
|
114
|
+
{ headers: apiHeaders(opts) },
|
|
115
|
+
);
|
|
116
|
+
if (!res.ok) return;
|
|
117
|
+
const data = (await res.json()) as {
|
|
118
|
+
cancelled?: Array<{ id: string; failureReason?: string }>;
|
|
119
|
+
};
|
|
120
|
+
const isCancelled = data.cancelled?.some((t) => t.id === taskId);
|
|
121
|
+
if (isCancelled) {
|
|
122
|
+
opts.abortRef.current?.abort();
|
|
123
|
+
if (opts.onCancel) {
|
|
124
|
+
try {
|
|
125
|
+
await opts.onCancel();
|
|
126
|
+
} catch {
|
|
127
|
+
// Swallow — best-effort.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// Swallow — fire-and-forget.
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const checkLoop = (toolName: string, args: unknown): void => {
|
|
138
|
+
const taskId = opts.taskId;
|
|
139
|
+
if (!taskId) return;
|
|
140
|
+
const argRecord = args && typeof args === "object" ? (args as Record<string, unknown>) : {};
|
|
141
|
+
void checkToolLoop(taskId, toolName, argRecord)
|
|
142
|
+
.then((result) => {
|
|
143
|
+
if (result.blocked) {
|
|
144
|
+
opts.abortRef.current?.abort();
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
.catch(() => {});
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const heartbeat = (): void => {
|
|
151
|
+
if (opts.taskId && shouldRun("heartbeat", HEARTBEAT_THROTTLE_MS)) {
|
|
152
|
+
fireAndForget(
|
|
153
|
+
`${opts.apiUrl}/api/active-sessions/heartbeat/${encodeURIComponent(opts.taskId)}`,
|
|
154
|
+
{ method: "PUT", headers: apiHeaders(opts) },
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const activity = (): void => {
|
|
160
|
+
if (shouldRun("activity", ACTIVITY_THROTTLE_MS)) {
|
|
161
|
+
fireAndForget(`${opts.apiUrl}/api/agents/${encodeURIComponent(opts.agentId)}/activity`, {
|
|
162
|
+
method: "PUT",
|
|
163
|
+
headers: apiHeaders(opts),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const progressContextUsage = (event: {
|
|
169
|
+
contextUsedTokens: number;
|
|
170
|
+
contextTotalTokens: number;
|
|
171
|
+
contextPercent: number;
|
|
172
|
+
}): void => {
|
|
173
|
+
if (opts.taskId && shouldRun("context-progress", CONTEXT_THROTTLE_MS)) {
|
|
174
|
+
fireAndForget(`${opts.apiUrl}/api/tasks/${encodeURIComponent(opts.taskId)}/context`, {
|
|
175
|
+
method: "POST",
|
|
176
|
+
headers: apiHeaders(opts),
|
|
177
|
+
body: JSON.stringify({
|
|
178
|
+
eventType: "progress",
|
|
179
|
+
sessionId: sessionId ?? `${opts.sessionIdFallbackPrefix ?? "session"}-${opts.taskId}`,
|
|
180
|
+
contextUsedTokens: event.contextUsedTokens,
|
|
181
|
+
contextTotalTokens: event.contextTotalTokens,
|
|
182
|
+
contextPercent: event.contextPercent,
|
|
183
|
+
}),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const progressCompaction = (event: {
|
|
189
|
+
contextTotalTokens: number;
|
|
190
|
+
preCompactTokens?: number;
|
|
191
|
+
compactTrigger?: string;
|
|
192
|
+
}): void => {
|
|
193
|
+
if (opts.taskId) {
|
|
194
|
+
fireAndForget(`${opts.apiUrl}/api/tasks/${encodeURIComponent(opts.taskId)}/context`, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: apiHeaders(opts),
|
|
197
|
+
body: JSON.stringify({
|
|
198
|
+
eventType: "compaction",
|
|
199
|
+
sessionId: sessionId ?? `${opts.sessionIdFallbackPrefix ?? "session"}-${opts.taskId}`,
|
|
200
|
+
contextTotalTokens: event.contextTotalTokens,
|
|
201
|
+
preCompactTokens: event.preCompactTokens,
|
|
202
|
+
compactTrigger: event.compactTrigger,
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const progressCompletion = (): void => {
|
|
209
|
+
if (opts.taskId) {
|
|
210
|
+
fireAndForget(`${opts.apiUrl}/api/tasks/${encodeURIComponent(opts.taskId)}/context`, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: apiHeaders(opts),
|
|
213
|
+
body: JSON.stringify({
|
|
214
|
+
eventType: "completion",
|
|
215
|
+
sessionId: sessionId ?? `${opts.sessionIdFallbackPrefix ?? "session"}-${opts.taskId}`,
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return (event: ProviderEvent): void => {
|
|
222
|
+
try {
|
|
223
|
+
switch (event.type) {
|
|
224
|
+
case "session_init": {
|
|
225
|
+
sessionId = event.sessionId;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case "tool_start": {
|
|
229
|
+
if (shouldRun("cancellation", CANCELLATION_THROTTLE_MS)) {
|
|
230
|
+
checkCancelled();
|
|
231
|
+
}
|
|
232
|
+
checkLoop(event.toolName, event.args);
|
|
233
|
+
heartbeat();
|
|
234
|
+
activity();
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case "context_usage": {
|
|
238
|
+
progressContextUsage({
|
|
239
|
+
contextUsedTokens: event.contextUsedTokens,
|
|
240
|
+
contextTotalTokens: event.contextTotalTokens,
|
|
241
|
+
contextPercent: event.contextPercent,
|
|
242
|
+
});
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case "compaction": {
|
|
246
|
+
progressCompaction({
|
|
247
|
+
contextTotalTokens: event.contextTotalTokens,
|
|
248
|
+
preCompactTokens: event.preCompactTokens,
|
|
249
|
+
compactTrigger: event.compactTrigger,
|
|
250
|
+
});
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case "result": {
|
|
254
|
+
progressCompletion();
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
// Never throw from the handler — the event loop is hot.
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
package/src/providers/types.ts
CHANGED
|
@@ -12,11 +12,25 @@ export interface CostData {
|
|
|
12
12
|
numTurns: number;
|
|
13
13
|
model: string;
|
|
14
14
|
isError: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Phase 6: tells the API which recompute path to use on
|
|
17
|
+
* `POST /api/session-costs`. Codex triggers the pricing-table recompute
|
|
18
|
+
* (when DB pricing rows exist for all three token classes); Claude / pi
|
|
19
|
+
* always trust the harness-reported `totalCostUsd` as-is.
|
|
20
|
+
*/
|
|
21
|
+
provider?: "claude" | "codex" | "pi";
|
|
15
22
|
}
|
|
16
23
|
|
|
24
|
+
import type { ProviderName } from "../types";
|
|
25
|
+
|
|
17
26
|
/** Normalized event emitted by any provider adapter. */
|
|
18
27
|
export type ProviderEvent =
|
|
19
|
-
| {
|
|
28
|
+
| {
|
|
29
|
+
type: "session_init";
|
|
30
|
+
sessionId: string;
|
|
31
|
+
provider?: ProviderName;
|
|
32
|
+
providerMeta?: Record<string, unknown>;
|
|
33
|
+
}
|
|
20
34
|
| { type: "message"; role: "assistant" | "user"; content: string }
|
|
21
35
|
| { type: "tool_start"; toolCallId: string; toolName: string; args: unknown }
|
|
22
36
|
| { type: "tool_end"; toolCallId: string; toolName: string; result: unknown }
|
|
@@ -24,6 +38,7 @@ export type ProviderEvent =
|
|
|
24
38
|
| { type: "error"; message: string; category?: string }
|
|
25
39
|
| { type: "raw_log"; content: string }
|
|
26
40
|
| { type: "raw_stderr"; content: string }
|
|
41
|
+
| { type: "progress"; message: string }
|
|
27
42
|
| { type: "custom"; name: string; data: unknown }
|
|
28
43
|
| {
|
|
29
44
|
type: "context_usage";
|
|
@@ -50,6 +65,7 @@ export interface ProviderSessionConfig {
|
|
|
50
65
|
apiUrl: string;
|
|
51
66
|
apiKey: string;
|
|
52
67
|
cwd: string;
|
|
68
|
+
vcsRepo?: string;
|
|
53
69
|
resumeSessionId?: string;
|
|
54
70
|
iteration?: number;
|
|
55
71
|
logFile: string;
|
|
@@ -79,9 +95,18 @@ export interface ProviderResult {
|
|
|
79
95
|
failureReason?: string;
|
|
80
96
|
}
|
|
81
97
|
|
|
98
|
+
/** Behavioral traits that govern prompt assembly and feature gating. */
|
|
99
|
+
export interface ProviderTraits {
|
|
100
|
+
/** Provider can call MCP tools (store-progress, task-action, skills, slack-reply, etc.) */
|
|
101
|
+
hasMcp: boolean;
|
|
102
|
+
/** Provider runs in the local Docker container with /workspace, identity files, agent-fs, PM2, etc. */
|
|
103
|
+
hasLocalEnvironment: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
82
106
|
/** Main contract for a harness provider adapter. */
|
|
83
107
|
export interface ProviderAdapter {
|
|
84
108
|
readonly name: string;
|
|
109
|
+
readonly traits: ProviderTraits;
|
|
85
110
|
createSession(config: ProviderSessionConfig): Promise<ProviderSession>;
|
|
86
111
|
canResume(sessionId: string): Promise<boolean>;
|
|
87
112
|
formatCommand(commandName: string): string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { type BasePromptArgs, getBasePrompt } from "../prompts/base-prompt";
|
|
3
|
+
import type { ProviderTraits } from "../providers/types";
|
|
3
4
|
|
|
4
5
|
/** Minimal valid args to reduce boilerplate */
|
|
5
6
|
const minimalArgs: BasePromptArgs = {
|
|
@@ -334,3 +335,201 @@ describe("getBasePrompt — truncation", () => {
|
|
|
334
335
|
expect(result).not.toContain("[...truncated");
|
|
335
336
|
});
|
|
336
337
|
});
|
|
338
|
+
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// Remote provider (no MCP, no local environment) — trait-aware prompt assembly
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
const remoteTraits: ProviderTraits = { hasMcp: false, hasLocalEnvironment: false };
|
|
343
|
+
const remoteProviderArgs: BasePromptArgs = {
|
|
344
|
+
...minimalArgs,
|
|
345
|
+
traits: remoteTraits,
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
describe("getBasePrompt — remote provider composite selection", () => {
|
|
349
|
+
test("uses remote worker composite (not generic worker)", async () => {
|
|
350
|
+
const result = await getBasePrompt(remoteProviderArgs);
|
|
351
|
+
// Remote worker template says "output is captured automatically"
|
|
352
|
+
expect(result).toContain("output is captured automatically");
|
|
353
|
+
// Should NOT contain generic worker tools
|
|
354
|
+
expect(result).not.toContain("store-progress");
|
|
355
|
+
expect(result).not.toContain("task-action");
|
|
356
|
+
expect(result).not.toContain("read-messages");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("still includes role and agentId", async () => {
|
|
360
|
+
const result = await getBasePrompt(remoteProviderArgs);
|
|
361
|
+
expect(result).toContain("worker");
|
|
362
|
+
expect(result).toContain("agent-abc-123");
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe("getBasePrompt — remote provider excluded sections", () => {
|
|
367
|
+
test("excludes join-swarm / register instructions", async () => {
|
|
368
|
+
const result = await getBasePrompt(remoteProviderArgs);
|
|
369
|
+
expect(result).not.toContain("join-swarm");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("excludes /workspace filesystem layout", async () => {
|
|
373
|
+
const result = await getBasePrompt(remoteProviderArgs);
|
|
374
|
+
expect(result).not.toContain("/workspace/personal");
|
|
375
|
+
expect(result).not.toContain("/workspace/shared");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("excludes How You Are Built section", async () => {
|
|
379
|
+
const result = await getBasePrompt(remoteProviderArgs);
|
|
380
|
+
expect(result).not.toContain("How You Are Built");
|
|
381
|
+
expect(result).not.toContain("hooks fire during your session");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("excludes context mode tools", async () => {
|
|
385
|
+
const result = await getBasePrompt(remoteProviderArgs);
|
|
386
|
+
expect(result).not.toContain("context-mode");
|
|
387
|
+
expect(result).not.toContain("batch_execute");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("excludes system packages section", async () => {
|
|
391
|
+
const result = await getBasePrompt(remoteProviderArgs);
|
|
392
|
+
expect(result).not.toContain("sudo apt-get install");
|
|
393
|
+
expect(result).not.toContain("System packages available");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("excludes VCS CLI tools table", async () => {
|
|
397
|
+
const result = await getBasePrompt(remoteProviderArgs);
|
|
398
|
+
expect(result).not.toContain("glab mr create");
|
|
399
|
+
expect(result).not.toContain("gh pr create");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("excludes service registry / PM2", async () => {
|
|
403
|
+
const result = await getBasePrompt(remoteProviderArgs);
|
|
404
|
+
expect(result).not.toContain("Service Registry");
|
|
405
|
+
expect(result).not.toContain("pm2 start");
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("excludes code quality section", async () => {
|
|
409
|
+
const result = await getBasePrompt(remoteProviderArgs);
|
|
410
|
+
expect(result).not.toContain("Code Quality");
|
|
411
|
+
expect(result).not.toContain("--no-verify");
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("excludes capabilities listing", async () => {
|
|
415
|
+
const result = await getBasePrompt({
|
|
416
|
+
...remoteProviderArgs,
|
|
417
|
+
capabilities: ["core", "task-pool"],
|
|
418
|
+
});
|
|
419
|
+
expect(result).not.toContain("Capabilities enabled");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("skips Slack instructions even with slackContext", async () => {
|
|
423
|
+
const result = await getBasePrompt({
|
|
424
|
+
...remoteProviderArgs,
|
|
425
|
+
slackContext: { channelId: "C123", threadTs: "123.456" },
|
|
426
|
+
});
|
|
427
|
+
expect(result).not.toContain("slack-reply");
|
|
428
|
+
expect(result).not.toContain("C123");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("skips skills section even when provided", async () => {
|
|
432
|
+
const result = await getBasePrompt({
|
|
433
|
+
...remoteProviderArgs,
|
|
434
|
+
skillsSummary: [{ name: "commit", description: "Create a commit" }],
|
|
435
|
+
});
|
|
436
|
+
expect(result).not.toContain("Installed Skills");
|
|
437
|
+
expect(result).not.toContain("/commit");
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("skips MCP servers section even when provided", async () => {
|
|
441
|
+
const result = await getBasePrompt({
|
|
442
|
+
...remoteProviderArgs,
|
|
443
|
+
mcpServersSummary: "- my-server: http://localhost:3000",
|
|
444
|
+
});
|
|
445
|
+
expect(result).not.toContain("Installed MCP Servers");
|
|
446
|
+
expect(result).not.toContain("my-server");
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("skips CLAUDE.md and TOOLS.md truncatable sections", async () => {
|
|
450
|
+
const result = await getBasePrompt({
|
|
451
|
+
...remoteProviderArgs,
|
|
452
|
+
claudeMd: "# Agent instructions here",
|
|
453
|
+
toolsMd: "# Tools here",
|
|
454
|
+
});
|
|
455
|
+
expect(result).not.toContain("Agent Instructions");
|
|
456
|
+
expect(result).not.toContain("Agent instructions here");
|
|
457
|
+
expect(result).not.toContain("Your Tools & Capabilities");
|
|
458
|
+
expect(result).not.toContain("Tools here");
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe("getBasePrompt — remote provider identity", () => {
|
|
463
|
+
test("uses simplified identity (no SOUL.md / IDENTITY.md)", async () => {
|
|
464
|
+
const result = await getBasePrompt({
|
|
465
|
+
...remoteProviderArgs,
|
|
466
|
+
name: "remote-worker-1",
|
|
467
|
+
description: "A remote worker",
|
|
468
|
+
soulMd: "# SOUL.md content that should NOT appear",
|
|
469
|
+
identityMd: "# IDENTITY.md content that should NOT appear",
|
|
470
|
+
});
|
|
471
|
+
expect(result).toContain("**Name:** remote-worker-1");
|
|
472
|
+
expect(result).toContain("**Description:** A remote worker");
|
|
473
|
+
expect(result).toContain("Desplega platform");
|
|
474
|
+
// Identity files should NOT be injected
|
|
475
|
+
expect(result).not.toContain("SOUL.md content that should NOT appear");
|
|
476
|
+
expect(result).not.toContain("IDENTITY.md content that should NOT appear");
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("identity section present even without name", async () => {
|
|
480
|
+
const result = await getBasePrompt(remoteProviderArgs);
|
|
481
|
+
expect(result).toContain("Your Identity");
|
|
482
|
+
expect(result).toContain("Desplega platform");
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
describe("getBasePrompt — remote provider keeps repo context", () => {
|
|
487
|
+
test("skips CLAUDE.md content for remote providers", async () => {
|
|
488
|
+
const result = await getBasePrompt({
|
|
489
|
+
...remoteProviderArgs,
|
|
490
|
+
repoContext: {
|
|
491
|
+
claudeMd: "Run `bun test` before pushing.",
|
|
492
|
+
clonePath: "/workspace/repos/my-repo",
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
expect(result).toContain("Repository Context");
|
|
496
|
+
// Remote providers don't get claudeMd injected
|
|
497
|
+
expect(result).not.toContain("Run `bun test` before pushing.");
|
|
498
|
+
expect(result).not.toContain("/workspace/repos/my-repo");
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("includes repo guidelines", async () => {
|
|
502
|
+
const result = await getBasePrompt({
|
|
503
|
+
...remoteProviderArgs,
|
|
504
|
+
repoContext: {
|
|
505
|
+
clonePath: "/workspace/repos/my-repo",
|
|
506
|
+
guidelines: {
|
|
507
|
+
prChecks: ["bun run lint:fix", "bun test"],
|
|
508
|
+
mergeChecks: [],
|
|
509
|
+
allowMerge: false,
|
|
510
|
+
review: [],
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
expect(result).toContain("Repository Guidelines");
|
|
515
|
+
expect(result).toContain("bun run lint:fix");
|
|
516
|
+
expect(result).toContain("bun test");
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
describe("getBasePrompt — local providers unaffected", () => {
|
|
521
|
+
test("local provider uses generic worker composite", async () => {
|
|
522
|
+
const result = await getBasePrompt({
|
|
523
|
+
...minimalArgs,
|
|
524
|
+
traits: { hasMcp: true, hasLocalEnvironment: true },
|
|
525
|
+
});
|
|
526
|
+
expect(result).toContain("store-progress");
|
|
527
|
+
expect(result).toContain("/workspace");
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("undefined traits defaults to local provider behavior", async () => {
|
|
531
|
+
const result = await getBasePrompt(minimalArgs);
|
|
532
|
+
expect(result).toContain("store-progress");
|
|
533
|
+
expect(result).toContain("/workspace");
|
|
534
|
+
});
|
|
535
|
+
});
|