@swarmclawai/swarmclaw 0.7.6 → 0.7.8
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 +19 -10
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +16 -0
- package/src/app/api/agents/route.ts +2 -0
- package/src/app/api/chats/[id]/route.ts +21 -1
- package/src/app/api/chats/route.ts +13 -1
- package/src/app/api/connectors/[id]/route.ts +20 -2
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +3 -0
- package/src/app/api/external-agents/[id]/route.ts +38 -6
- package/src/app/api/external-agents/route.ts +17 -1
- package/src/app/api/gateways/[id]/health/route.ts +8 -0
- package/src/app/api/gateways/[id]/route.ts +53 -1
- package/src/app/api/gateways/route.ts +53 -0
- package/src/app/api/openclaw/deploy/route.ts +139 -0
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/cli/index.js +40 -0
- package/src/cli/index.test.js +68 -0
- package/src/cli/spec.js +60 -0
- package/src/components/agents/agent-sheet.tsx +281 -33
- package/src/components/auth/setup-wizard.tsx +75 -2
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-header.tsx +4 -0
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/gateways/gateway-sheet.tsx +140 -8
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/openclaw/openclaw-deploy-panel.tsx +591 -9
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +221 -17
- package/src/components/shared/settings/section-capability-policy.tsx +38 -0
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/tasks/approvals-panel.tsx +177 -18
- package/src/components/tasks/task-board.tsx +137 -23
- package/src/components/tasks/task-card.tsx +29 -0
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/lib/server/agent-runtime-config.ts +142 -7
- package/src/lib/server/agent-thread-session.ts +9 -1
- package/src/lib/server/capability-router.test.ts +22 -0
- package/src/lib/server/capability-router.ts +54 -18
- package/src/lib/server/chat-execution.ts +33 -3
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.ts +99 -74
- package/src/lib/server/daemon-state.ts +83 -46
- package/src/lib/server/elevenlabs.test.ts +59 -1
- package/src/lib/server/heartbeat-service.ts +5 -1
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/openclaw-deploy.test.ts +8 -0
- package/src/lib/server/openclaw-deploy.ts +679 -19
- package/src/lib/server/orchestrator-lg.ts +1 -0
- package/src/lib/server/orchestrator.ts +11 -0
- package/src/lib/server/plugins.ts +6 -1
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-followups.test.ts +147 -2
- package/src/lib/server/queue.ts +278 -8
- package/src/lib/server/session-run-manager.ts +31 -0
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.ts +26 -1
- package/src/lib/server/session-tools/context.ts +5 -0
- package/src/lib/server/session-tools/crud.ts +265 -76
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +38 -2
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.ts +14 -2
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/web-inputs.test.ts +17 -0
- package/src/lib/server/session-tools/web.ts +153 -6
- package/src/lib/server/stream-agent-chat.test.ts +27 -2
- package/src/lib/server/stream-agent-chat.ts +104 -30
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +269 -0
- package/src/lib/setup-defaults.ts +2 -2
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/validation/schemas.ts +9 -0
- package/src/types/index.ts +104 -0
|
@@ -7,6 +7,7 @@ import { spawnSync } from 'child_process'
|
|
|
7
7
|
import * as cheerio from 'cheerio'
|
|
8
8
|
import {
|
|
9
9
|
loadAgents, saveAgents,
|
|
10
|
+
loadProjects, saveProjects,
|
|
10
11
|
loadTasks, saveTasks,
|
|
11
12
|
loadSchedules, saveSchedules,
|
|
12
13
|
loadSkills, saveSkills,
|
|
@@ -31,9 +32,11 @@ import {
|
|
|
31
32
|
} from '@/lib/server/agent-assignment'
|
|
32
33
|
import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
|
|
33
34
|
import { normalizeSchedulePayload } from '@/lib/server/schedule-normalization'
|
|
35
|
+
import { buildProjectSnapshot, ensureProjectWorkspace, normalizeProjectCreateInput, normalizeProjectPatchInput } from '@/lib/server/project-utils'
|
|
34
36
|
import type { ToolBuildContext } from './context'
|
|
35
37
|
import { safePath, findBinaryOnPath } from './context'
|
|
36
38
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
39
|
+
import type { BoardTask } from '@/types'
|
|
37
40
|
|
|
38
41
|
// ---------------------------------------------------------------------------
|
|
39
42
|
// Document helpers
|
|
@@ -132,6 +135,98 @@ function normalizeTaskStatusInput(status: unknown, prevStatus?: string): string
|
|
|
132
135
|
return normalized
|
|
133
136
|
}
|
|
134
137
|
|
|
138
|
+
function normalizeTaskIdList(value: unknown): string[] {
|
|
139
|
+
const rawValues = Array.isArray(value)
|
|
140
|
+
? value
|
|
141
|
+
: typeof value === 'string'
|
|
142
|
+
? value.split(',')
|
|
143
|
+
: []
|
|
144
|
+
const seen = new Set<string>()
|
|
145
|
+
const out: string[] = []
|
|
146
|
+
for (const entry of rawValues) {
|
|
147
|
+
const normalized = typeof entry === 'string' ? entry.trim() : ''
|
|
148
|
+
if (!normalized || seen.has(normalized)) continue
|
|
149
|
+
seen.add(normalized)
|
|
150
|
+
out.push(normalized)
|
|
151
|
+
}
|
|
152
|
+
return out
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function pickFirstTaskId(value: unknown): string | null {
|
|
156
|
+
const ids = normalizeTaskIdList(value)
|
|
157
|
+
return ids[0] || null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function applyTaskContinuationDefaults(
|
|
161
|
+
parsed: Record<string, unknown>,
|
|
162
|
+
tasks: Record<string, BoardTask>,
|
|
163
|
+
explicitInput?: Record<string, unknown>,
|
|
164
|
+
): string | null {
|
|
165
|
+
const explicit = explicitInput || parsed
|
|
166
|
+
const continuationTaskId = pickFirstTaskId(parsed.continueFromTaskId)
|
|
167
|
+
|| pickFirstTaskId(parsed.followUpToTaskId)
|
|
168
|
+
|| pickFirstTaskId(parsed.resumeFromTaskId)
|
|
169
|
+
const blockedBy = [
|
|
170
|
+
...normalizeTaskIdList(parsed.blockedBy),
|
|
171
|
+
...normalizeTaskIdList(parsed.dependsOn),
|
|
172
|
+
...normalizeTaskIdList(parsed.dependsOnTaskIds),
|
|
173
|
+
...normalizeTaskIdList(parsed.prerequisiteTaskIds),
|
|
174
|
+
]
|
|
175
|
+
if (continuationTaskId && !blockedBy.includes(continuationTaskId)) {
|
|
176
|
+
blockedBy.unshift(continuationTaskId)
|
|
177
|
+
}
|
|
178
|
+
if (blockedBy.length > 0) parsed.blockedBy = blockedBy
|
|
179
|
+
|
|
180
|
+
if (continuationTaskId) {
|
|
181
|
+
const sourceTask = tasks[continuationTaskId]
|
|
182
|
+
if (!sourceTask) return `Error: source task "${continuationTaskId}" not found.`
|
|
183
|
+
|
|
184
|
+
if (!Object.prototype.hasOwnProperty.call(explicit, 'projectId') && typeof sourceTask.projectId === 'string' && sourceTask.projectId.trim()) {
|
|
185
|
+
parsed.projectId = sourceTask.projectId.trim()
|
|
186
|
+
}
|
|
187
|
+
if (
|
|
188
|
+
!Object.prototype.hasOwnProperty.call(explicit, 'agentId')
|
|
189
|
+
&& !hasManagedAgentAssignmentInput(explicit)
|
|
190
|
+
&& typeof sourceTask.agentId === 'string'
|
|
191
|
+
&& sourceTask.agentId.trim()
|
|
192
|
+
) {
|
|
193
|
+
parsed.agentId = sourceTask.agentId.trim()
|
|
194
|
+
}
|
|
195
|
+
if (!Object.prototype.hasOwnProperty.call(explicit, 'cwd') && typeof sourceTask.cwd === 'string' && sourceTask.cwd.trim()) {
|
|
196
|
+
parsed.cwd = sourceTask.cwd.trim()
|
|
197
|
+
}
|
|
198
|
+
const sourceSessionId = typeof sourceTask.checkpoint?.lastSessionId === 'string' && sourceTask.checkpoint.lastSessionId.trim()
|
|
199
|
+
? sourceTask.checkpoint.lastSessionId.trim()
|
|
200
|
+
: typeof sourceTask.sessionId === 'string' && sourceTask.sessionId.trim()
|
|
201
|
+
? sourceTask.sessionId.trim()
|
|
202
|
+
: ''
|
|
203
|
+
if (!Object.prototype.hasOwnProperty.call(explicit, 'sessionId') && sourceSessionId) {
|
|
204
|
+
parsed.sessionId = sourceSessionId
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const resumeFieldMap: Array<[keyof BoardTask, string]> = [
|
|
208
|
+
['cliResumeId', 'cliResumeId'],
|
|
209
|
+
['cliProvider', 'cliProvider'],
|
|
210
|
+
['claudeResumeId', 'claudeResumeId'],
|
|
211
|
+
['codexResumeId', 'codexResumeId'],
|
|
212
|
+
['opencodeResumeId', 'opencodeResumeId'],
|
|
213
|
+
['geminiResumeId', 'geminiResumeId'],
|
|
214
|
+
]
|
|
215
|
+
for (const [sourceKey, targetKey] of resumeFieldMap) {
|
|
216
|
+
const value = sourceTask[sourceKey]
|
|
217
|
+
if (Object.prototype.hasOwnProperty.call(explicit, targetKey)) continue
|
|
218
|
+
if (typeof value === 'string' && value.trim()) {
|
|
219
|
+
parsed[targetKey] = value.trim()
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const aliasKey of ['continueFromTaskId', 'followUpToTaskId', 'resumeFromTaskId', 'dependsOn', 'dependsOnTaskIds', 'prerequisiteTaskIds']) {
|
|
225
|
+
delete parsed[aliasKey]
|
|
226
|
+
}
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
|
|
135
230
|
// ---------------------------------------------------------------------------
|
|
136
231
|
// RESOURCE_DEFAULTS
|
|
137
232
|
// ---------------------------------------------------------------------------
|
|
@@ -212,6 +307,7 @@ const RESOURCE_DEFAULTS: Record<string, (parsed: any) => any> = {
|
|
|
212
307
|
agentIds: Array.isArray(p.agentIds) ? p.agentIds : [],
|
|
213
308
|
...p,
|
|
214
309
|
}),
|
|
310
|
+
manage_projects: (p) => normalizeProjectCreateInput(p),
|
|
215
311
|
}
|
|
216
312
|
|
|
217
313
|
// ---------------------------------------------------------------------------
|
|
@@ -226,6 +322,7 @@ const PLATFORM_RESOURCES: Record<string, {
|
|
|
226
322
|
readOnly?: boolean
|
|
227
323
|
}> = {
|
|
228
324
|
manage_agents: { toolId: 'manage_agents', label: 'agents', load: loadAgents, save: saveAgents },
|
|
325
|
+
manage_projects: { toolId: 'manage_projects', label: 'projects', load: loadProjects, save: saveProjects },
|
|
229
326
|
manage_tasks: { toolId: 'manage_tasks', label: 'tasks', load: loadTasks, save: saveTasks },
|
|
230
327
|
manage_schedules: { toolId: 'manage_schedules', label: 'schedules', load: loadSchedules, save: saveSchedules },
|
|
231
328
|
manage_skills: { toolId: 'manage_skills', label: 'skills', load: loadSkills, save: saveSkills },
|
|
@@ -242,6 +339,14 @@ const PLATFORM_RESOURCES: Record<string, {
|
|
|
242
339
|
export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
243
340
|
const tools: StructuredToolInterface[] = []
|
|
244
341
|
const { cwd, ctx, hasPlugin } = bctx
|
|
342
|
+
const buildCrudPayload = (normalized: Record<string, unknown>, action: string | undefined, data: string | undefined): Record<string, unknown> => {
|
|
343
|
+
if (data) return JSON.parse(data)
|
|
344
|
+
if (action !== 'create' && action !== 'update') return {}
|
|
345
|
+
const entries = Object.entries(normalized).filter(([key]) =>
|
|
346
|
+
!['action', 'id', 'data', 'resource', 'input', 'args', 'arguments', 'payload', 'parameters'].includes(key),
|
|
347
|
+
)
|
|
348
|
+
return entries.length > 0 ? Object.fromEntries(entries) : {}
|
|
349
|
+
}
|
|
245
350
|
|
|
246
351
|
// Build dynamic agent summary for tools that need agent awareness
|
|
247
352
|
const assignScope = ctx?.platformAssignScope || 'self'
|
|
@@ -271,6 +376,17 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
271
376
|
} else {
|
|
272
377
|
description += `\n\nYou may create tasks for yourself, leave them unassigned, or delegate them to other agents. Your agent ID is "${ctx?.agentId || 'unknown'}". When delegating, set a target agent using "agentId", "assignee", "agent", "assignedAgentId", or "assigned_agent_id". Use the target agent's exact ID when possible. Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.` + agentSummary
|
|
273
378
|
}
|
|
379
|
+
description += '\n\nCreate/update calls accept either `data` as a JSON string or direct top-level fields like `title`, `description`, `status`, `agentId`, and `projectId`.'
|
|
380
|
+
description += '\n\nFor follow-up work, set `continueFromTaskId` (or `followUpToTaskId`) to a prior task ID. The new task will inherit the predecessor\'s project/agent/session context, block on that task by default, and reuse its execution session when possible.'
|
|
381
|
+
if (ctx?.projectId) {
|
|
382
|
+
description += `\n\nCurrent project context: "${ctx.projectName || ctx.projectId}" (projectId "${ctx.projectId}"). Omit "projectId" to use this active project by default.`
|
|
383
|
+
}
|
|
384
|
+
} else if (toolKey === 'manage_projects') {
|
|
385
|
+
description += '\n\nProjects hold durable execution context for longer-lived work: objective, audience, pilot priorities, open objectives, credential requirements, and preferred heartbeat cadence.'
|
|
386
|
+
description += '\n\nCreate/update calls accept either `data` as a JSON string or direct top-level fields like `name`, `description`, `objective`, `audience`, `priorities`, `openObjectives`, `capabilityHints`, `credentialRequirements`, `heartbeatPrompt`, and `heartbeatIntervalSec`.'
|
|
387
|
+
if (ctx?.projectId) {
|
|
388
|
+
description += `\n\nCurrent project context: "${ctx.projectName || ctx.projectId}" (projectId "${ctx.projectId}"). For get/update/delete, you may omit "id" to target this active project.`
|
|
389
|
+
}
|
|
274
390
|
} else if (toolKey === 'manage_agents') {
|
|
275
391
|
description += `\n\nAgents may self-edit their own soul. To update your soul, use action="update", id="${ctx?.agentId || 'your-agent-id'}", and include data with the "soul" field. Set "platformAssignScope":"all" to let an agent delegate work across the fleet; use "self" for solo execution.`
|
|
276
392
|
} else if (toolKey === 'manage_schedules') {
|
|
@@ -279,8 +395,16 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
279
395
|
} else {
|
|
280
396
|
description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set "agentId" to another agent when needed. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).` + agentSummary
|
|
281
397
|
}
|
|
398
|
+
if (ctx?.projectId) {
|
|
399
|
+
description += `\n\nCurrent project context: "${ctx.projectName || ctx.projectId}" (projectId "${ctx.projectId}"). Omit "projectId" to use this active project by default.`
|
|
400
|
+
}
|
|
282
401
|
} else if (toolKey === 'manage_webhooks') {
|
|
283
402
|
description += '\n\nUse `source`, `events`, `agentId`, and `secret` when creating webhooks. Inbound calls should POST to `/api/webhooks/{id}` with header `x-webhook-secret` when a secret is configured.'
|
|
403
|
+
} else if (toolKey === 'manage_secrets') {
|
|
404
|
+
description += '\n\nUse this for credential bootstrapping and durable secret storage. Create/update calls accept either `data` as JSON or direct top-level fields like `name`, `service`, `value`, `scope`, `agentIds`, and `projectId`.'
|
|
405
|
+
if (ctx?.projectId) {
|
|
406
|
+
description += `\n\nCurrent project context: "${ctx.projectName || ctx.projectId}" (projectId "${ctx.projectId}"). Omit "projectId" to link the secret to this active project.`
|
|
407
|
+
}
|
|
284
408
|
}
|
|
285
409
|
|
|
286
410
|
tools.push(
|
|
@@ -298,6 +422,11 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
298
422
|
}
|
|
299
423
|
try {
|
|
300
424
|
if (action === 'list') {
|
|
425
|
+
if (toolKey === 'manage_projects') {
|
|
426
|
+
const values = Object.values(res.load())
|
|
427
|
+
.map((project: any) => buildProjectSnapshot(project))
|
|
428
|
+
return JSON.stringify(values)
|
|
429
|
+
}
|
|
301
430
|
if (toolKey === 'manage_secrets') {
|
|
302
431
|
const values = Object.values(res.load())
|
|
303
432
|
.filter((s: any) => canAccessSecret(s))
|
|
@@ -307,6 +436,7 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
307
436
|
service: s.service,
|
|
308
437
|
scope: s.scope || 'global',
|
|
309
438
|
agentIds: s.agentIds || [],
|
|
439
|
+
projectId: s.projectId || null,
|
|
310
440
|
createdAt: s.createdAt,
|
|
311
441
|
updatedAt: s.updatedAt,
|
|
312
442
|
}))
|
|
@@ -315,40 +445,56 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
315
445
|
return JSON.stringify(Object.values(res.load()))
|
|
316
446
|
}
|
|
317
447
|
if (action === 'get') {
|
|
318
|
-
|
|
448
|
+
const effectiveId = id || (toolKey === 'manage_projects' ? ctx?.projectId || undefined : undefined)
|
|
449
|
+
if (!effectiveId) return 'Error: "id" is required for get action.'
|
|
319
450
|
const all = res.load()
|
|
320
|
-
if (!all[
|
|
451
|
+
if (!all[effectiveId]) return `Not found: ${res.label} "${effectiveId}"`
|
|
452
|
+
if (toolKey === 'manage_projects') {
|
|
453
|
+
return JSON.stringify(buildProjectSnapshot(all[effectiveId]))
|
|
454
|
+
}
|
|
321
455
|
if (toolKey === 'manage_secrets') {
|
|
322
|
-
if (!canAccessSecret(all[
|
|
456
|
+
if (!canAccessSecret(all[effectiveId])) return 'Error: you do not have access to this secret.'
|
|
323
457
|
let value = ''
|
|
324
458
|
try {
|
|
325
|
-
value = all[
|
|
459
|
+
value = all[effectiveId].encryptedValue ? decryptKey(all[effectiveId].encryptedValue) : ''
|
|
326
460
|
} catch {
|
|
327
461
|
value = ''
|
|
328
462
|
}
|
|
329
463
|
return JSON.stringify({
|
|
330
|
-
id: all[
|
|
331
|
-
name: all[
|
|
332
|
-
service: all[
|
|
333
|
-
scope: all[
|
|
334
|
-
agentIds: all[
|
|
464
|
+
id: all[effectiveId].id,
|
|
465
|
+
name: all[effectiveId].name,
|
|
466
|
+
service: all[effectiveId].service,
|
|
467
|
+
scope: all[effectiveId].scope || 'global',
|
|
468
|
+
agentIds: all[effectiveId].agentIds || [],
|
|
469
|
+
projectId: all[effectiveId].projectId || null,
|
|
335
470
|
value,
|
|
336
|
-
createdAt: all[
|
|
337
|
-
updatedAt: all[
|
|
471
|
+
createdAt: all[effectiveId].createdAt,
|
|
472
|
+
updatedAt: all[effectiveId].updatedAt,
|
|
338
473
|
})
|
|
339
474
|
}
|
|
340
|
-
return JSON.stringify(all[
|
|
475
|
+
return JSON.stringify(all[effectiveId])
|
|
341
476
|
}
|
|
342
477
|
if (res.readOnly) return `Cannot ${action} ${res.label} via this tool (read-only).`
|
|
343
478
|
if (action === 'create') {
|
|
344
479
|
const all = res.load()
|
|
345
|
-
const raw =
|
|
480
|
+
const raw = buildCrudPayload(normalized, action, data)
|
|
346
481
|
const defaults = RESOURCE_DEFAULTS[toolKey]
|
|
347
482
|
const parsed = defaults ? defaults(raw) : raw
|
|
348
483
|
if (parsed && typeof parsed === 'object' && 'id' in parsed) {
|
|
349
484
|
delete (parsed as Record<string, unknown>).id
|
|
350
485
|
}
|
|
351
486
|
const now = Date.now()
|
|
487
|
+
if (toolKey === 'manage_tasks') {
|
|
488
|
+
const continuationError = applyTaskContinuationDefaults(
|
|
489
|
+
parsed as Record<string, unknown>,
|
|
490
|
+
all as Record<string, BoardTask>,
|
|
491
|
+
raw as Record<string, unknown>,
|
|
492
|
+
)
|
|
493
|
+
if (continuationError) return continuationError
|
|
494
|
+
}
|
|
495
|
+
if ((toolKey === 'manage_tasks' || toolKey === 'manage_schedules' || toolKey === 'manage_secrets') && !Object.prototype.hasOwnProperty.call(parsed, 'projectId') && ctx?.projectId) {
|
|
496
|
+
parsed.projectId = ctx.projectId
|
|
497
|
+
}
|
|
352
498
|
if (toolKey === 'manage_tasks' || toolKey === 'manage_schedules') {
|
|
353
499
|
const agents = loadAgents()
|
|
354
500
|
const resolution = resolveManagedAgentAssignment(
|
|
@@ -455,7 +601,7 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
455
601
|
createdAt: now,
|
|
456
602
|
updatedAt: now,
|
|
457
603
|
}
|
|
458
|
-
let responseEntry:
|
|
604
|
+
let responseEntry: unknown = entry
|
|
459
605
|
if (toolKey === 'manage_secrets') {
|
|
460
606
|
const secretValue = typeof parsed.value === 'string' ? parsed.value : null
|
|
461
607
|
if (!secretValue) return 'Error: data.value is required to create a secret.'
|
|
@@ -470,12 +616,17 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
470
616
|
...entry,
|
|
471
617
|
scope: normalizedScope,
|
|
472
618
|
agentIds: normalizedAgentIds,
|
|
619
|
+
projectId: typeof parsed.projectId === 'string' && parsed.projectId.trim() ? parsed.projectId.trim() : undefined,
|
|
473
620
|
encryptedValue: encryptKey(secretValue),
|
|
474
621
|
}
|
|
475
622
|
delete (stored as any).value
|
|
476
623
|
all[newId] = stored
|
|
477
624
|
const { encryptedValue, ...safe } = stored
|
|
478
625
|
responseEntry = safe
|
|
626
|
+
} else if (toolKey === 'manage_projects') {
|
|
627
|
+
all[newId] = entry
|
|
628
|
+
ensureProjectWorkspace(newId, entry.name)
|
|
629
|
+
responseEntry = buildProjectSnapshot(entry)
|
|
479
630
|
} else {
|
|
480
631
|
all[newId] = entry
|
|
481
632
|
}
|
|
@@ -510,30 +661,38 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
510
661
|
return JSON.stringify(responseEntry)
|
|
511
662
|
}
|
|
512
663
|
if (action === 'update') {
|
|
513
|
-
|
|
664
|
+
const effectiveId = id || (toolKey === 'manage_projects' ? ctx?.projectId || undefined : undefined)
|
|
665
|
+
if (!effectiveId) return 'Error: "id" is required for update action.'
|
|
514
666
|
const all = res.load()
|
|
515
|
-
if (!all[
|
|
516
|
-
const parsed =
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
667
|
+
if (!all[effectiveId]) return `Not found: ${res.label} "${effectiveId}"`
|
|
668
|
+
const parsed = toolKey === 'manage_projects'
|
|
669
|
+
? normalizeProjectPatchInput(buildCrudPayload(normalized, action, data))
|
|
670
|
+
: buildCrudPayload(normalized, action, data)
|
|
671
|
+
const parsedRecord = parsed as Record<string, unknown>
|
|
672
|
+
if (toolKey === 'manage_tasks') {
|
|
673
|
+
const continuationError = applyTaskContinuationDefaults(parsedRecord, all as Record<string, BoardTask>, parsedRecord)
|
|
674
|
+
if (continuationError) return continuationError
|
|
675
|
+
}
|
|
676
|
+
const prevStatus = all[effectiveId]?.status
|
|
677
|
+
if (toolKey === 'manage_tasks' && Object.prototype.hasOwnProperty.call(parsedRecord, 'status')) {
|
|
678
|
+
const normalized = normalizeTaskStatusInput(parsedRecord.status, prevStatus)
|
|
679
|
+
if (normalized) parsedRecord.status = normalized
|
|
680
|
+
else delete parsedRecord.status
|
|
522
681
|
}
|
|
523
|
-
if (toolKey === 'manage_tasks' && Object.prototype.hasOwnProperty.call(
|
|
682
|
+
if (toolKey === 'manage_tasks' && Object.prototype.hasOwnProperty.call(parsedRecord, 'qualityGate')) {
|
|
524
683
|
const settings = loadSettings()
|
|
525
|
-
|
|
526
|
-
? normalizeTaskQualityGate(
|
|
684
|
+
parsedRecord.qualityGate = parsedRecord.qualityGate
|
|
685
|
+
? normalizeTaskQualityGate(parsedRecord.qualityGate, settings)
|
|
527
686
|
: null
|
|
528
687
|
}
|
|
529
688
|
if (toolKey === 'manage_tasks' || toolKey === 'manage_schedules') {
|
|
530
689
|
const agents = loadAgents()
|
|
531
|
-
const requestedClear = Object.prototype.hasOwnProperty.call(
|
|
690
|
+
const requestedClear = Object.prototype.hasOwnProperty.call(parsedRecord, 'agentId') && parsedRecord.agentId == null
|
|
532
691
|
const shouldResolveAssignment = requestedClear
|
|
533
|
-
|| hasManagedAgentAssignmentInput(
|
|
692
|
+
|| hasManagedAgentAssignmentInput(parsedRecord)
|
|
534
693
|
if (shouldResolveAssignment) {
|
|
535
694
|
const resolution = resolveManagedAgentAssignment(
|
|
536
|
-
|
|
695
|
+
parsedRecord,
|
|
537
696
|
agents,
|
|
538
697
|
null,
|
|
539
698
|
{ allowDescription: false },
|
|
@@ -547,108 +706,138 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
547
706
|
unresolvedReference: requestedClear ? null : resolution.unresolvedReference,
|
|
548
707
|
isDelegation: toolKey === 'manage_tasks'
|
|
549
708
|
? isDelegationTaskPayload({
|
|
550
|
-
...all[
|
|
551
|
-
...
|
|
709
|
+
...all[effectiveId],
|
|
710
|
+
...parsedRecord,
|
|
552
711
|
agentId: requestedClear ? null : resolution.agentId,
|
|
553
712
|
} as Record<string, unknown>)
|
|
554
713
|
: false,
|
|
555
714
|
delegatorAgentId: toolKey === 'manage_tasks'
|
|
556
715
|
? resolveDelegatorAgentId({
|
|
557
|
-
...all[
|
|
558
|
-
...
|
|
559
|
-
}
|
|
716
|
+
...all[effectiveId],
|
|
717
|
+
...parsedRecord,
|
|
718
|
+
}, agents, ctx?.agentId || null)
|
|
560
719
|
: null,
|
|
561
720
|
})
|
|
562
721
|
if (assignmentError) return assignmentError
|
|
563
|
-
if (!requestedClear)
|
|
722
|
+
if (!requestedClear) parsedRecord.agentId = resolution.agentId
|
|
564
723
|
}
|
|
565
724
|
}
|
|
566
|
-
all[
|
|
725
|
+
all[effectiveId] = { ...all[effectiveId], ...parsed, updatedAt: Date.now() }
|
|
567
726
|
if (toolKey === 'manage_schedules') {
|
|
568
|
-
const normalizedSchedule = normalizeSchedulePayload(all[
|
|
727
|
+
const normalizedSchedule = normalizeSchedulePayload(all[effectiveId] as Record<string, unknown>, {
|
|
569
728
|
cwd,
|
|
570
729
|
now: Date.now(),
|
|
571
730
|
})
|
|
572
731
|
if (!normalizedSchedule.ok) return normalizedSchedule.error
|
|
573
|
-
all[
|
|
574
|
-
...all[
|
|
732
|
+
all[effectiveId] = {
|
|
733
|
+
...all[effectiveId],
|
|
575
734
|
...normalizedSchedule.value,
|
|
576
735
|
updatedAt: Date.now(),
|
|
577
736
|
}
|
|
578
737
|
}
|
|
579
738
|
if (toolKey === 'manage_secrets') {
|
|
580
|
-
if (!canAccessSecret(all[
|
|
581
|
-
const nextScope =
|
|
739
|
+
if (!canAccessSecret(all[effectiveId])) return 'Error: you do not have access to this secret.'
|
|
740
|
+
const nextScope = parsedRecord.scope === 'agent'
|
|
582
741
|
? 'agent'
|
|
583
|
-
:
|
|
742
|
+
: parsedRecord.scope === 'global'
|
|
584
743
|
? 'global'
|
|
585
|
-
: (all[
|
|
744
|
+
: (all[effectiveId].scope === 'agent' ? 'agent' : 'global')
|
|
586
745
|
if (nextScope === 'agent') {
|
|
587
|
-
const incomingIds = Array.isArray(
|
|
588
|
-
?
|
|
589
|
-
: Array.isArray(all[
|
|
590
|
-
? all[
|
|
746
|
+
const incomingIds = Array.isArray(parsedRecord.agentIds)
|
|
747
|
+
? parsedRecord.agentIds.filter((x: any) => typeof x === 'string')
|
|
748
|
+
: Array.isArray(all[effectiveId].agentIds)
|
|
749
|
+
? all[effectiveId].agentIds
|
|
591
750
|
: []
|
|
592
|
-
all[
|
|
751
|
+
all[effectiveId].agentIds = Array.from(new Set([
|
|
593
752
|
...incomingIds,
|
|
594
753
|
...(ctx?.agentId ? [ctx.agentId] : []),
|
|
595
754
|
]))
|
|
596
755
|
} else {
|
|
597
|
-
all[
|
|
756
|
+
all[effectiveId].agentIds = []
|
|
757
|
+
}
|
|
758
|
+
all[effectiveId].scope = nextScope
|
|
759
|
+
if (Object.prototype.hasOwnProperty.call(parsedRecord, 'projectId')) {
|
|
760
|
+
all[effectiveId].projectId = typeof parsedRecord.projectId === 'string' && parsedRecord.projectId.trim()
|
|
761
|
+
? parsedRecord.projectId.trim()
|
|
762
|
+
: undefined
|
|
598
763
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
all[id].encryptedValue = encryptKey(parsed.value)
|
|
764
|
+
if (typeof parsedRecord.value === 'string' && parsedRecord.value.trim()) {
|
|
765
|
+
all[effectiveId].encryptedValue = encryptKey(parsedRecord.value)
|
|
602
766
|
}
|
|
603
|
-
delete all[
|
|
767
|
+
delete all[effectiveId].value
|
|
604
768
|
}
|
|
605
769
|
|
|
606
|
-
if (toolKey === 'manage_tasks' && all[
|
|
770
|
+
if (toolKey === 'manage_tasks' && all[effectiveId].status === 'completed') {
|
|
607
771
|
const { formatValidationFailure, validateTaskCompletion } = await import('../task-validation')
|
|
608
772
|
const { ensureTaskCompletionReport } = await import('../task-reports')
|
|
609
773
|
const settings = loadSettings()
|
|
610
|
-
const report = ensureTaskCompletionReport(all[
|
|
611
|
-
if (report?.relativePath) (all[
|
|
612
|
-
const validation = validateTaskCompletion(all[
|
|
613
|
-
;(all[
|
|
774
|
+
const report = ensureTaskCompletionReport(all[effectiveId] as any)
|
|
775
|
+
if (report?.relativePath) (all[effectiveId] as any).completionReportPath = report.relativePath
|
|
776
|
+
const validation = validateTaskCompletion(all[effectiveId] as any, { report, settings })
|
|
777
|
+
;(all[effectiveId] as any).validation = validation
|
|
614
778
|
if (!validation.ok) {
|
|
615
|
-
all[
|
|
616
|
-
;(all[
|
|
617
|
-
;(all[
|
|
618
|
-
} else if ((all[
|
|
619
|
-
;(all[
|
|
779
|
+
all[effectiveId].status = 'failed'
|
|
780
|
+
;(all[effectiveId] as any).completedAt = null
|
|
781
|
+
;(all[effectiveId] as any).error = formatValidationFailure(validation.reasons).slice(0, 500)
|
|
782
|
+
} else if ((all[effectiveId] as any).completedAt == null) {
|
|
783
|
+
;(all[effectiveId] as any).completedAt = Date.now()
|
|
620
784
|
}
|
|
621
785
|
}
|
|
622
786
|
|
|
623
787
|
res.save(all)
|
|
624
|
-
if (toolKey === '
|
|
788
|
+
if (toolKey === 'manage_projects') {
|
|
789
|
+
ensureProjectWorkspace(effectiveId, all[effectiveId].name)
|
|
790
|
+
}
|
|
791
|
+
if (toolKey === 'manage_tasks' && prevStatus !== 'queued' && all[effectiveId].status === 'queued') {
|
|
625
792
|
const { enqueueTask } = await import('../queue')
|
|
626
|
-
enqueueTask(
|
|
793
|
+
enqueueTask(effectiveId)
|
|
627
794
|
} else if (
|
|
628
795
|
toolKey === 'manage_tasks'
|
|
629
|
-
&& prevStatus !== all[
|
|
630
|
-
&& (all[
|
|
631
|
-
&& all[
|
|
796
|
+
&& prevStatus !== all[effectiveId].status
|
|
797
|
+
&& (all[effectiveId].status === 'completed' || all[effectiveId].status === 'failed')
|
|
798
|
+
&& all[effectiveId].sessionId
|
|
632
799
|
) {
|
|
633
800
|
const { disableSessionHeartbeat } = await import('../queue')
|
|
634
|
-
disableSessionHeartbeat(all[
|
|
801
|
+
disableSessionHeartbeat(all[effectiveId].sessionId)
|
|
635
802
|
}
|
|
636
803
|
if (toolKey === 'manage_secrets') {
|
|
637
|
-
const { encryptedValue, ...safe } = all[
|
|
804
|
+
const { encryptedValue, ...safe } = all[effectiveId]
|
|
638
805
|
return JSON.stringify(safe)
|
|
639
806
|
}
|
|
640
|
-
|
|
807
|
+
if (toolKey === 'manage_projects') {
|
|
808
|
+
return JSON.stringify(buildProjectSnapshot(all[effectiveId]))
|
|
809
|
+
}
|
|
810
|
+
return JSON.stringify(all[effectiveId])
|
|
641
811
|
}
|
|
642
812
|
if (action === 'delete') {
|
|
643
|
-
|
|
813
|
+
const effectiveId = id || (toolKey === 'manage_projects' ? ctx?.projectId || undefined : undefined)
|
|
814
|
+
if (!effectiveId) return 'Error: "id" is required for delete action.'
|
|
644
815
|
const all = res.load()
|
|
645
|
-
if (!all[
|
|
646
|
-
if (toolKey === 'manage_secrets' && !canAccessSecret(all[
|
|
816
|
+
if (!all[effectiveId]) return `Not found: ${res.label} "${effectiveId}"`
|
|
817
|
+
if (toolKey === 'manage_secrets' && !canAccessSecret(all[effectiveId])) {
|
|
647
818
|
return 'Error: you do not have access to this secret.'
|
|
648
819
|
}
|
|
649
|
-
delete all[
|
|
820
|
+
delete all[effectiveId]
|
|
650
821
|
res.save(all)
|
|
651
|
-
|
|
822
|
+
if (toolKey === 'manage_projects') {
|
|
823
|
+
const clearProjectId = (load: () => Record<string, Record<string, unknown>>, save: (d: Record<string, Record<string, unknown>>) => void) => {
|
|
824
|
+
const items = load()
|
|
825
|
+
let changed = false
|
|
826
|
+
for (const item of Object.values(items)) {
|
|
827
|
+
if (item.projectId === effectiveId) {
|
|
828
|
+
item.projectId = undefined
|
|
829
|
+
changed = true
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (changed) save(items)
|
|
833
|
+
}
|
|
834
|
+
clearProjectId(loadAgents, saveAgents)
|
|
835
|
+
clearProjectId(loadTasks, saveTasks)
|
|
836
|
+
clearProjectId(loadSchedules, saveSchedules)
|
|
837
|
+
clearProjectId(loadSkills, saveSkills)
|
|
838
|
+
clearProjectId(loadSecrets, saveSecrets)
|
|
839
|
+
}
|
|
840
|
+
return JSON.stringify({ deleted: effectiveId })
|
|
652
841
|
}
|
|
653
842
|
return `Unknown action "${action}". Valid: list, get, create, update, delete`
|
|
654
843
|
} catch (err: any) {
|
|
@@ -662,7 +851,7 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
662
851
|
action: z.enum(['list', 'get', 'create', 'update', 'delete']).describe('The CRUD action to perform'),
|
|
663
852
|
id: z.string().optional().describe('Resource ID (required for get, update, delete)'),
|
|
664
853
|
data: z.string().optional().describe('JSON string of fields for create/update'),
|
|
665
|
-
}),
|
|
854
|
+
}).passthrough(),
|
|
666
855
|
},
|
|
667
856
|
),
|
|
668
857
|
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { resolveDelegateResumeConfig } from './delegate'
|
|
4
|
+
|
|
5
|
+
describe('resolveDelegateResumeConfig', () => {
|
|
6
|
+
it('auto-resumes when a stored backend resume ID exists', () => {
|
|
7
|
+
const config = resolveDelegateResumeConfig(
|
|
8
|
+
{ task: 'continue the implementation' },
|
|
9
|
+
'codex',
|
|
10
|
+
{
|
|
11
|
+
readStoredDelegateResumeId: (key) => key === 'codex' ? 'codex-thread-42' : null,
|
|
12
|
+
},
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
assert.deepEqual(config, {
|
|
16
|
+
resume: true,
|
|
17
|
+
resumeId: '',
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('respects explicit resume=false even when a stored ID exists', () => {
|
|
22
|
+
const config = resolveDelegateResumeConfig(
|
|
23
|
+
{ task: 'start fresh', resume: false },
|
|
24
|
+
'claude',
|
|
25
|
+
{
|
|
26
|
+
readStoredDelegateResumeId: () => 'claude-session-99',
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
assert.deepEqual(config, {
|
|
31
|
+
resume: false,
|
|
32
|
+
resumeId: '',
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('treats an explicit resumeId as an instruction to resume immediately', () => {
|
|
37
|
+
const config = resolveDelegateResumeConfig(
|
|
38
|
+
{ task: 'continue', resumeId: 'gemini-session-5' },
|
|
39
|
+
'gemini',
|
|
40
|
+
{
|
|
41
|
+
readStoredDelegateResumeId: () => null,
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
assert.deepEqual(config, {
|
|
46
|
+
resume: true,
|
|
47
|
+
resumeId: 'gemini-session-5',
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -247,12 +247,48 @@ function bindDelegateRuntime(runtime: DelegateRuntimeState | undefined, child: C
|
|
|
247
247
|
child.once('error', clear)
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
function coerceOptionalBool(value: unknown): boolean | null {
|
|
251
|
+
if (typeof value === 'boolean') return value
|
|
252
|
+
if (typeof value === 'string') {
|
|
253
|
+
const normalized = value.trim().toLowerCase()
|
|
254
|
+
if (['true', '1', 'yes', 'on'].includes(normalized)) return true
|
|
255
|
+
if (['false', '0', 'no', 'off'].includes(normalized)) return false
|
|
256
|
+
}
|
|
257
|
+
return null
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function resumeStorageKeyForBackend(
|
|
261
|
+
backend: 'claude' | 'codex' | 'opencode' | 'gemini',
|
|
262
|
+
): 'claudeCode' | 'codex' | 'opencode' | 'gemini' {
|
|
263
|
+
if (backend === 'claude') return 'claudeCode'
|
|
264
|
+
if (backend === 'codex') return 'codex'
|
|
265
|
+
if (backend === 'opencode') return 'opencode'
|
|
266
|
+
return 'gemini'
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function resolveDelegateResumeConfig(
|
|
270
|
+
normalized: Record<string, unknown>,
|
|
271
|
+
backend: 'claude' | 'codex' | 'opencode' | 'gemini',
|
|
272
|
+
bctx: { readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini') => string | null },
|
|
273
|
+
): { resume: boolean; resumeId: string } {
|
|
274
|
+
const explicitResumeId = typeof normalized.resumeId === 'string' ? normalized.resumeId.trim() : ''
|
|
275
|
+
if (explicitResumeId) return { resume: true, resumeId: explicitResumeId }
|
|
276
|
+
|
|
277
|
+
const explicitResume = coerceOptionalBool(normalized.resume)
|
|
278
|
+
if (explicitResume !== null) return { resume: explicitResume, resumeId: '' }
|
|
279
|
+
|
|
280
|
+
const storedResumeId = bctx.readStoredDelegateResumeId?.(resumeStorageKeyForBackend(backend))
|
|
281
|
+
return {
|
|
282
|
+
resume: Boolean(storedResumeId),
|
|
283
|
+
resumeId: '',
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
250
287
|
async function runDelegateBackend(args: Record<string, unknown>, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
|
|
251
288
|
const normalized = normalizeDelegateArgs(args)
|
|
252
289
|
const task = normalized.task as string
|
|
253
290
|
const backend = ((normalized.backend as string) || 'claude') as DelegateBackend
|
|
254
|
-
const resume = normalized
|
|
255
|
-
const resumeId = normalized.resumeId as string
|
|
291
|
+
const { resume, resumeId } = resolveDelegateResumeConfig(normalized, backend, bctx)
|
|
256
292
|
const backends = {
|
|
257
293
|
claude: findBinaryOnPath('claude'),
|
|
258
294
|
codex: findBinaryOnPath('codex'),
|