whipped 0.0.1 → 0.1.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 +202 -4
- package/dist/cli.js +28695 -0
- package/dist/mcp-server.js +2552 -0
- package/dist/migrations/001_initial.sql +101 -0
- package/dist/migrations/002_board_state.sql +134 -0
- package/dist/migrations/003_workflow_pk_fix.sql +81 -0
- package/dist/migrations/004_review_comments_id_pk.sql +50 -0
- package/dist/migrations/005_card_dep_no_self.sql +28 -0
- package/dist/migrations/006_memory.sql +76 -0
- package/dist/migrations/007_drop_always_inject_project_state.sql +10 -0
- package/dist/migrations/008_memory_tags.sql +52 -0
- package/dist/migrations/009_card_relations.sql +44 -0
- package/dist/migrations/010_review_comment_stable_id.sql +17 -0
- package/dist/migrations/011_card_plan_model_config.sql +14 -0
- package/dist/migrations/012_recurring_agents.sql +54 -0
- package/dist/web-ui/assets/index-Dgu7Q26n.css +7946 -0
- package/dist/web-ui/assets/index-rpsRpaUU.js +81526 -0
- package/dist/web-ui/assets/logo-mJL-8LYD.png +0 -0
- package/dist/web-ui/favicon.png +0 -0
- package/dist/web-ui/index.html +14 -0
- package/package.json +70 -4
- package/scripts/postinstall.mjs +43 -0
|
@@ -0,0 +1,2552 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __ovrCreateRequire } from "node:module";
|
|
3
|
+
import { fileURLToPath as __ovrFileURLToPath } from "node:url";
|
|
4
|
+
const require = __ovrCreateRequire(import.meta.url);
|
|
5
|
+
const __filename = __ovrFileURLToPath(import.meta.url);
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/core/api-contract.ts
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
function resolveWorkflowForCard(workflows, card) {
|
|
19
|
+
const isStory = card.type === "story";
|
|
20
|
+
return workflows.find((w) => w.id === card.workflowId) ?? workflows.find((w) => w.isDefault && w.forStory === isStory) ?? workflows.find((w) => w.forStory === isStory) ?? workflows[0];
|
|
21
|
+
}
|
|
22
|
+
function snapshotModelConfig(workflow) {
|
|
23
|
+
const config = {};
|
|
24
|
+
for (const slot of workflow?.slots ?? []) {
|
|
25
|
+
config[slot.id] = { pairs: slot.pairs, mode: slot.mode };
|
|
26
|
+
}
|
|
27
|
+
return config;
|
|
28
|
+
}
|
|
29
|
+
function highestWorkflowLevel(workflow) {
|
|
30
|
+
let bestIdx = -1;
|
|
31
|
+
for (const slot of workflow?.slots ?? []) {
|
|
32
|
+
for (const p of slot.pairs) bestIdx = Math.max(bestIdx, LEVEL_ORDER.indexOf(p.level));
|
|
33
|
+
}
|
|
34
|
+
return LEVEL_ORDER[bestIdx] ?? "medium";
|
|
35
|
+
}
|
|
36
|
+
var runtimeAgentIdSchema, effortLevelSchema, agentModelChoiceSchema, workflowSlotTypeSchema, tierLevelSchema, LEVEL_ORDER, modelPairSchema, pairSelectionModeSchema, SLOT_TOOL_IDS, slotToolSchema, slotModelConfigSchema, cardModelConfigSchema, promptValueSchema, EMPTY_INLINE_PROMPT, workflowSlotSchema, DEFAULT_MODEL_PAIR, DEFAULT_SLOT_MODEL_FIELDS, workflowSchema, DEFAULT_WORKFLOW, DEFAULT_STORY_WORKFLOW, DEFAULT_GIT_INSTRUCTIONS, runtimeBoardColumnIdSchema, BOARD_COLUMNS, reviewActorSchema, reviewIssueSchema, reviewAttachmentSchema, runtimeReviewCommentSchema, runtimeActivityEntrySchema, runtimeTaskSessionStateSchema, runtimeTerminalSessionEntrySchema, runtimeCardPrioritySchema, cardTypeSchema, runtimePrMetaSchema, runtimeBoardCardSchema, runtimeBoardColumnSchema, runtimeBoardDataSchema, runtimeGlobalConfigSchema, runtimeGithubConfigSchema, runtimeWorktreeSetupSchema, runtimeProjectSecretSchema, runtimeProjectConfigSchema, runtimeWorkspaceStateResponseSchema, runtimeWorkspaceStateSaveRequestSchema, runtimeVisualElementSchema, runtimeVisualCommentSchema, runtimeCardCreateRequestSchema, runtimeCardMoveRequestSchema, runtimeCardUpdateRequestSchema, memoryScopeSchema, memoryTypeSchema, memorySourceTypeSchema, memoryStatusSchema, runtimeMemoryOriginAgentSchema, runtimeMemorySchema, recurringScheduleKindSchema, recurringScheduleSchema, recurringRunStatusSchema, recurringRunTriggerSchema, recurringAgentRunSchema, recurringAgentSchema, recurringAgentCreateRequestSchema, recurringAgentUpdateRequestSchema, projectFolderSchema, topLevelItemSchema, projectsLayoutSchema, runtimeProjectSchema;
|
|
37
|
+
var init_api_contract = __esm({
|
|
38
|
+
"src/core/api-contract.ts"() {
|
|
39
|
+
"use strict";
|
|
40
|
+
runtimeAgentIdSchema = z.enum(["claude", "codex", "opencode", "cursor"]);
|
|
41
|
+
effortLevelSchema = z.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
42
|
+
agentModelChoiceSchema = z.object({
|
|
43
|
+
agentId: runtimeAgentIdSchema.default("claude"),
|
|
44
|
+
model: z.string().nullable().optional(),
|
|
45
|
+
effort: effortLevelSchema.nullable().optional()
|
|
46
|
+
});
|
|
47
|
+
workflowSlotTypeSchema = z.enum(["dev", "review", "plan", "orch"]);
|
|
48
|
+
tierLevelSchema = z.enum(["minimal", "low", "medium", "high", "max"]);
|
|
49
|
+
LEVEL_ORDER = ["minimal", "low", "medium", "high", "max"];
|
|
50
|
+
modelPairSchema = z.object({
|
|
51
|
+
id: z.string(),
|
|
52
|
+
level: tierLevelSchema,
|
|
53
|
+
isFree: z.boolean().default(false),
|
|
54
|
+
binary: runtimeAgentIdSchema,
|
|
55
|
+
model: z.string().nullable().optional(),
|
|
56
|
+
effort: effortLevelSchema.nullable().optional()
|
|
57
|
+
});
|
|
58
|
+
pairSelectionModeSchema = z.enum(["auto", "preferFree", "freeOnly", "paidOnly"]);
|
|
59
|
+
SLOT_TOOL_IDS = ["browser"];
|
|
60
|
+
slotToolSchema = z.enum(SLOT_TOOL_IDS);
|
|
61
|
+
slotModelConfigSchema = z.object({
|
|
62
|
+
pairs: z.array(modelPairSchema).min(1),
|
|
63
|
+
mode: pairSelectionModeSchema.default("auto"),
|
|
64
|
+
pinnedPairId: z.string().optional()
|
|
65
|
+
});
|
|
66
|
+
cardModelConfigSchema = z.record(z.string(), slotModelConfigSchema);
|
|
67
|
+
promptValueSchema = z.preprocess(
|
|
68
|
+
(v) => {
|
|
69
|
+
if (typeof v === "string") return { source: "inline", text: v };
|
|
70
|
+
return v;
|
|
71
|
+
},
|
|
72
|
+
z.discriminatedUnion("source", [
|
|
73
|
+
z.object({ source: z.literal("inline"), text: z.string() }),
|
|
74
|
+
z.object({ source: z.literal("file"), path: z.string() })
|
|
75
|
+
])
|
|
76
|
+
);
|
|
77
|
+
EMPTY_INLINE_PROMPT = { source: "inline", text: "" };
|
|
78
|
+
workflowSlotSchema = z.object({
|
|
79
|
+
id: z.string(),
|
|
80
|
+
type: workflowSlotTypeSchema,
|
|
81
|
+
name: z.string(),
|
|
82
|
+
order: z.number().int().nonnegative(),
|
|
83
|
+
enabled: z.boolean(),
|
|
84
|
+
prompt: promptValueSchema.default(EMPTY_INLINE_PROMPT),
|
|
85
|
+
// Model tiers for this slot, in priority order (top = highest). Copied to the
|
|
86
|
+
// card at creation; the card's active level + mode select which pair runs.
|
|
87
|
+
pairs: z.array(modelPairSchema).min(1),
|
|
88
|
+
mode: pairSelectionModeSchema.default("auto"),
|
|
89
|
+
// Tools this slot may use (e.g. "browser"). Workflow-only, not ticket-editable.
|
|
90
|
+
tools: z.array(slotToolSchema).default([]),
|
|
91
|
+
// review slots only: may set the card's active level on reopen.
|
|
92
|
+
canAdjustLevel: z.boolean().default(false),
|
|
93
|
+
// plan slots only: re-run even if a plan already exists on the card.
|
|
94
|
+
rerun: z.boolean().default(false)
|
|
95
|
+
});
|
|
96
|
+
DEFAULT_MODEL_PAIR = {
|
|
97
|
+
id: "default",
|
|
98
|
+
level: "medium",
|
|
99
|
+
isFree: false,
|
|
100
|
+
binary: "claude",
|
|
101
|
+
model: null,
|
|
102
|
+
effort: null
|
|
103
|
+
};
|
|
104
|
+
DEFAULT_SLOT_MODEL_FIELDS = {
|
|
105
|
+
pairs: [DEFAULT_MODEL_PAIR],
|
|
106
|
+
mode: "auto"
|
|
107
|
+
};
|
|
108
|
+
workflowSchema = z.object({
|
|
109
|
+
id: z.string(),
|
|
110
|
+
name: z.string(),
|
|
111
|
+
isDefault: z.boolean().default(false),
|
|
112
|
+
forStory: z.boolean().default(false),
|
|
113
|
+
slots: z.array(workflowSlotSchema)
|
|
114
|
+
});
|
|
115
|
+
DEFAULT_WORKFLOW = {
|
|
116
|
+
id: "wf_default",
|
|
117
|
+
name: "Default",
|
|
118
|
+
isDefault: true,
|
|
119
|
+
forStory: false,
|
|
120
|
+
slots: [
|
|
121
|
+
{
|
|
122
|
+
id: "plan",
|
|
123
|
+
type: "plan",
|
|
124
|
+
name: "Plan",
|
|
125
|
+
order: 0,
|
|
126
|
+
enabled: false,
|
|
127
|
+
prompt: EMPTY_INLINE_PROMPT,
|
|
128
|
+
...DEFAULT_SLOT_MODEL_FIELDS,
|
|
129
|
+
tools: [],
|
|
130
|
+
canAdjustLevel: false,
|
|
131
|
+
rerun: false
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "dev",
|
|
135
|
+
type: "dev",
|
|
136
|
+
name: "Dev",
|
|
137
|
+
order: 1,
|
|
138
|
+
enabled: true,
|
|
139
|
+
prompt: EMPTY_INLINE_PROMPT,
|
|
140
|
+
...DEFAULT_SLOT_MODEL_FIELDS,
|
|
141
|
+
tools: [],
|
|
142
|
+
canAdjustLevel: false,
|
|
143
|
+
rerun: false
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "code_review",
|
|
147
|
+
type: "review",
|
|
148
|
+
name: "Code Review",
|
|
149
|
+
order: 2,
|
|
150
|
+
enabled: true,
|
|
151
|
+
prompt: EMPTY_INLINE_PROMPT,
|
|
152
|
+
...DEFAULT_SLOT_MODEL_FIELDS,
|
|
153
|
+
tools: [],
|
|
154
|
+
canAdjustLevel: false,
|
|
155
|
+
rerun: false
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "qa",
|
|
159
|
+
type: "review",
|
|
160
|
+
name: "QA",
|
|
161
|
+
order: 3,
|
|
162
|
+
enabled: false,
|
|
163
|
+
prompt: EMPTY_INLINE_PROMPT,
|
|
164
|
+
...DEFAULT_SLOT_MODEL_FIELDS,
|
|
165
|
+
tools: ["browser"],
|
|
166
|
+
canAdjustLevel: false,
|
|
167
|
+
rerun: false
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
};
|
|
171
|
+
DEFAULT_STORY_WORKFLOW = {
|
|
172
|
+
id: "wf_story_default",
|
|
173
|
+
name: "Story Default",
|
|
174
|
+
isDefault: true,
|
|
175
|
+
forStory: true,
|
|
176
|
+
slots: [
|
|
177
|
+
{
|
|
178
|
+
id: "orch",
|
|
179
|
+
type: "orch",
|
|
180
|
+
name: "Orchestrator",
|
|
181
|
+
order: 0,
|
|
182
|
+
enabled: true,
|
|
183
|
+
prompt: EMPTY_INLINE_PROMPT,
|
|
184
|
+
...DEFAULT_SLOT_MODEL_FIELDS,
|
|
185
|
+
tools: [],
|
|
186
|
+
canAdjustLevel: false,
|
|
187
|
+
rerun: false
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
};
|
|
191
|
+
DEFAULT_GIT_INSTRUCTIONS = `# Git conventions
|
|
192
|
+
|
|
193
|
+
These rules govern how to write commit messages, PR titles, and PR
|
|
194
|
+
descriptions.
|
|
195
|
+
|
|
196
|
+
## PR title
|
|
197
|
+
- Imperative, present tense: "Add board view", "Fix race in poller".
|
|
198
|
+
Not past tense, not gerund.
|
|
199
|
+
- \u226470 characters; aim for 50.
|
|
200
|
+
- Describe what shipped, not the task. "Add board view" beats
|
|
201
|
+
"Implement board view feature".
|
|
202
|
+
- No prefixes like \`feat:\` / \`[FEAT]\` / \`fix:\`.
|
|
203
|
+
- No ticket IDs in the title (put them in the description if needed).
|
|
204
|
+
- No trailing period.
|
|
205
|
+
|
|
206
|
+
## PR description
|
|
207
|
+
Keep it focused. Two sections, nothing more unless genuinely useful:
|
|
208
|
+
|
|
209
|
+
## Summary
|
|
210
|
+
- What changed and why. Use as many bullets as the scope warrants \u2014
|
|
211
|
+
a one-line fix is one bullet; a refactor touching 20 files may
|
|
212
|
+
need ten. Don't pad, don't truncate.
|
|
213
|
+
|
|
214
|
+
## Test plan
|
|
215
|
+
- What you actually ran or clicked, and the outcome.
|
|
216
|
+
- Type-check and lint passing are not a test plan on their own.
|
|
217
|
+
- If something couldn't be verified, say so in one line.
|
|
218
|
+
|
|
219
|
+
Do NOT include:
|
|
220
|
+
- Iteration narration ("Round N", "addressed feedback", "after review").
|
|
221
|
+
- Commit SHAs or branch names \u2014 GitHub already shows both.
|
|
222
|
+
- Paths to internal planning docs, scratch files, or task tracker URLs.
|
|
223
|
+
- "Verification:" sections that only list a passing type-check or lint.
|
|
224
|
+
- Self-congratulation ("clean", "all checks pass", "ready to merge").
|
|
225
|
+
- Restating the task description verbatim.
|
|
226
|
+
|
|
227
|
+
## Commit messages
|
|
228
|
+
- Short imperative subject line, \u226472 chars. That's usually enough.
|
|
229
|
+
- Skip the body unless a reviewer reading the diff alone would be
|
|
230
|
+
confused about *why* the change exists.
|
|
231
|
+
- Reference an issue only if a concrete one exists to close
|
|
232
|
+
(\`Closes #123\`). Never invent issue numbers.
|
|
233
|
+
`;
|
|
234
|
+
runtimeBoardColumnIdSchema = z.enum([
|
|
235
|
+
"todo",
|
|
236
|
+
"in_progress",
|
|
237
|
+
"reopened",
|
|
238
|
+
"ready_for_review",
|
|
239
|
+
"blocked",
|
|
240
|
+
"done"
|
|
241
|
+
]);
|
|
242
|
+
BOARD_COLUMNS = [
|
|
243
|
+
{ id: "todo", title: "Todo" },
|
|
244
|
+
{ id: "in_progress", title: "In Progress" },
|
|
245
|
+
{ id: "reopened", title: "Reopened" },
|
|
246
|
+
{ id: "ready_for_review", title: "Ready for Review" },
|
|
247
|
+
{ id: "blocked", title: "Blocked" },
|
|
248
|
+
{ id: "done", title: "Done" }
|
|
249
|
+
];
|
|
250
|
+
reviewActorSchema = z.object({
|
|
251
|
+
type: z.enum(["ai", "human", "external"]),
|
|
252
|
+
id: z.string(),
|
|
253
|
+
source: z.string().optional()
|
|
254
|
+
});
|
|
255
|
+
reviewIssueSchema = z.object({
|
|
256
|
+
file: z.string().optional(),
|
|
257
|
+
line: z.number().optional(),
|
|
258
|
+
severity: z.enum(["blocking", "warning", "info"]),
|
|
259
|
+
message: z.string()
|
|
260
|
+
});
|
|
261
|
+
reviewAttachmentSchema = z.object({
|
|
262
|
+
type: z.string(),
|
|
263
|
+
// "image" | "file" | any mime category
|
|
264
|
+
name: z.string(),
|
|
265
|
+
mimeType: z.string(),
|
|
266
|
+
path: z.string()
|
|
267
|
+
// absolute path in ~/.whipped/attachments/
|
|
268
|
+
});
|
|
269
|
+
runtimeReviewCommentSchema = z.object({
|
|
270
|
+
id: z.string(),
|
|
271
|
+
type: z.string(),
|
|
272
|
+
actor: reviewActorSchema,
|
|
273
|
+
status: z.enum(["pass", "fail", "warning", "skipped"]).optional(),
|
|
274
|
+
createdAt: z.number(),
|
|
275
|
+
streamId: z.string().optional(),
|
|
276
|
+
summary: z.string(),
|
|
277
|
+
issues: z.array(reviewIssueSchema).optional(),
|
|
278
|
+
attachments: z.array(reviewAttachmentSchema).optional(),
|
|
279
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
280
|
+
});
|
|
281
|
+
runtimeActivityEntrySchema = z.object({
|
|
282
|
+
timestamp: z.number(),
|
|
283
|
+
message: z.string()
|
|
284
|
+
});
|
|
285
|
+
runtimeTaskSessionStateSchema = z.enum(["running", "stopped", "completed", "failed", "killed"]);
|
|
286
|
+
runtimeTerminalSessionEntrySchema = z.object({
|
|
287
|
+
streamId: z.string(),
|
|
288
|
+
type: z.string(),
|
|
289
|
+
startedAt: z.number(),
|
|
290
|
+
endedAt: z.number().optional(),
|
|
291
|
+
agentId: runtimeAgentIdSchema.optional(),
|
|
292
|
+
state: runtimeTaskSessionStateSchema.optional()
|
|
293
|
+
});
|
|
294
|
+
runtimeCardPrioritySchema = z.enum(["urgent", "high", "medium", "low"]);
|
|
295
|
+
cardTypeSchema = z.enum(["task", "story", "subtask"]);
|
|
296
|
+
runtimePrMetaSchema = z.object({
|
|
297
|
+
url: z.string().optional(),
|
|
298
|
+
title: z.string().optional(),
|
|
299
|
+
description: z.string().optional(),
|
|
300
|
+
updatedAt: z.number().optional(),
|
|
301
|
+
updatedBy: z.string().optional()
|
|
302
|
+
});
|
|
303
|
+
runtimeBoardCardSchema = z.object({
|
|
304
|
+
id: z.string(),
|
|
305
|
+
description: z.string(),
|
|
306
|
+
descriptionAttachments: z.array(reviewAttachmentSchema).optional().default([]),
|
|
307
|
+
columnId: runtimeBoardColumnIdSchema,
|
|
308
|
+
type: cardTypeSchema.default("task"),
|
|
309
|
+
readyForDev: z.boolean().default(false),
|
|
310
|
+
agentId: runtimeAgentIdSchema.optional(),
|
|
311
|
+
priority: runtimeCardPrioritySchema.optional(),
|
|
312
|
+
// Single-parent stacking: this card continues in the parent's worktree/branch
|
|
313
|
+
// and starts once the parent reaches ready_for_review. Mutually exclusive with waitsFor.
|
|
314
|
+
dependsOn: z.string().optional(),
|
|
315
|
+
// Many-parent gate (tasks only): this card starts only once ALL listed cards are
|
|
316
|
+
// done (merged), in a fresh worktree branched from baseRef. Mutually exclusive with dependsOn.
|
|
317
|
+
waitsFor: z.array(z.string()).default([]),
|
|
318
|
+
// Story-only: the IDs of this story's subtasks. The story triggers its orchestrator
|
|
319
|
+
// workflow once every subtask reaches ready_for_review.
|
|
320
|
+
subtaskIds: z.array(z.string()).default([]),
|
|
321
|
+
autoFixAttempts: z.number().int().nonnegative().default(0),
|
|
322
|
+
baseRef: z.string(),
|
|
323
|
+
createdAt: z.number(),
|
|
324
|
+
updatedAt: z.number(),
|
|
325
|
+
githubIssueUrl: z.string().optional(),
|
|
326
|
+
pr: runtimePrMetaSchema.optional(),
|
|
327
|
+
workflowId: z.string().optional(),
|
|
328
|
+
// Plan written by the one-shot plan agent; injected into the dev agent's prompt.
|
|
329
|
+
plan: z.string().optional(),
|
|
330
|
+
// Workflow-wide capability level; every slot resolves it to its own pair.
|
|
331
|
+
activeLevel: tierLevelSchema.default("medium"),
|
|
332
|
+
// Per-slot model config, snapshotted from the workflow at creation and editable
|
|
333
|
+
// per ticket (slotId → {pairs, mode, pinnedPairId}).
|
|
334
|
+
modelConfig: cardModelConfigSchema.optional(),
|
|
335
|
+
reviewComments: z.array(runtimeReviewCommentSchema).default([]),
|
|
336
|
+
activityLog: z.array(runtimeActivityEntrySchema).default([]),
|
|
337
|
+
terminalSessions: z.array(runtimeTerminalSessionEntrySchema).default([]),
|
|
338
|
+
githubCommentIds: z.array(z.string()).default([]),
|
|
339
|
+
worktreePath: z.string().optional(),
|
|
340
|
+
branchName: z.string().optional(),
|
|
341
|
+
slackMessageTs: z.string().optional(),
|
|
342
|
+
slackChannelId: z.string().optional()
|
|
343
|
+
});
|
|
344
|
+
runtimeBoardColumnSchema = z.object({
|
|
345
|
+
id: runtimeBoardColumnIdSchema,
|
|
346
|
+
title: z.string(),
|
|
347
|
+
taskIds: z.array(z.string())
|
|
348
|
+
});
|
|
349
|
+
runtimeBoardDataSchema = z.object({
|
|
350
|
+
columns: z.array(runtimeBoardColumnSchema),
|
|
351
|
+
cards: z.record(z.string(), runtimeBoardCardSchema)
|
|
352
|
+
});
|
|
353
|
+
runtimeGlobalConfigSchema = z.object({
|
|
354
|
+
defaultAgent: runtimeAgentIdSchema.default("claude"),
|
|
355
|
+
maxParallelTasks: z.number().int().positive().default(4),
|
|
356
|
+
maxParallelQA: z.number().int().positive().default(1),
|
|
357
|
+
maxAutoFixAttempts: z.number().int().nonnegative().default(3),
|
|
358
|
+
pollingIntervalSeconds: z.number().int().positive().default(30),
|
|
359
|
+
prPollingIntervalSeconds: z.number().int().positive().default(60),
|
|
360
|
+
terminalApp: z.string().optional(),
|
|
361
|
+
slackEnabled: z.boolean().default(true),
|
|
362
|
+
slackBotToken: z.string().optional(),
|
|
363
|
+
slackSigningSecret: z.string().optional(),
|
|
364
|
+
slackAppConfigToken: z.string().optional(),
|
|
365
|
+
slackClientId: z.string().optional(),
|
|
366
|
+
slackClientSecret: z.string().optional(),
|
|
367
|
+
slackAppId: z.string().optional(),
|
|
368
|
+
slackOauthAuthorizeUrl: z.string().optional(),
|
|
369
|
+
slackPublicUrl: z.string().optional(),
|
|
370
|
+
slackBotName: z.string().default("Whipped"),
|
|
371
|
+
slackInstallerUserId: z.string().optional(),
|
|
372
|
+
autoStartTunnel: z.boolean().default(false),
|
|
373
|
+
tunnelId: z.string().optional(),
|
|
374
|
+
tunnelDomain: z.string().optional(),
|
|
375
|
+
tunnelName: z.string().default("whipped"),
|
|
376
|
+
// Auth: single shared password (scrypt hash) + HMAC secret for signed session
|
|
377
|
+
// cookies + machine token for local agent machinery (MCP/hooks). Never expose
|
|
378
|
+
// these over the API — see configController's response.
|
|
379
|
+
authPasswordHash: z.string().optional(),
|
|
380
|
+
authSessionSecret: z.string().optional(),
|
|
381
|
+
authMachineToken: z.string().optional()
|
|
382
|
+
});
|
|
383
|
+
runtimeGithubConfigSchema = z.object({
|
|
384
|
+
token: z.string()
|
|
385
|
+
});
|
|
386
|
+
runtimeWorktreeSetupSchema = z.object({
|
|
387
|
+
filesToCopy: z.array(z.string()).default([]),
|
|
388
|
+
installCommand: z.string().default("")
|
|
389
|
+
});
|
|
390
|
+
runtimeProjectSecretSchema = z.object({
|
|
391
|
+
key: z.string().min(1),
|
|
392
|
+
value: z.string()
|
|
393
|
+
});
|
|
394
|
+
runtimeProjectConfigSchema = z.object({
|
|
395
|
+
name: z.string().optional(),
|
|
396
|
+
defaultAgent: runtimeAgentIdSchema.optional(),
|
|
397
|
+
maxParallelTasks: z.number().int().positive().optional(),
|
|
398
|
+
maxAutoFixAttempts: z.number().int().nonnegative().optional(),
|
|
399
|
+
pollingIntervalSeconds: z.number().int().positive().optional(),
|
|
400
|
+
// What happens when a card passes review (polling/dispatch is always on; per-ticket
|
|
401
|
+
// readyForDev gates pickup). "off" parks it in ready_for_review, "pr" auto-creates a
|
|
402
|
+
// GitHub PR, "yolo" merges the branch straight into the local baseRef and pushes.
|
|
403
|
+
deliveryMode: z.enum(["off", "pr", "yolo"]).default("off"),
|
|
404
|
+
autoCommit: z.boolean().default(true),
|
|
405
|
+
defaultBaseBranch: z.string().optional(),
|
|
406
|
+
github: runtimeGithubConfigSchema.optional(),
|
|
407
|
+
worktreeSetup: runtimeWorktreeSetupSchema.optional(),
|
|
408
|
+
startCommand: z.string().default(""),
|
|
409
|
+
workflows: z.array(workflowSchema).default([]),
|
|
410
|
+
secrets: z.array(runtimeProjectSecretSchema).default([]),
|
|
411
|
+
systemPrompt: z.string().optional(),
|
|
412
|
+
// Freeform instructions injected into the dev agent's prompt to shape PR
|
|
413
|
+
// titles, descriptions, and commit messages. Empty/absent → daemon falls
|
|
414
|
+
// back to DEFAULT_GIT_INSTRUCTIONS.
|
|
415
|
+
gitInstructions: z.string().optional(),
|
|
416
|
+
// Which agent binary/model/effort the assistant agent runs as. Absent → claude.
|
|
417
|
+
assistantModel: agentModelChoiceSchema.optional()
|
|
418
|
+
});
|
|
419
|
+
runtimeWorkspaceStateResponseSchema = z.object({
|
|
420
|
+
workspaceId: z.string(),
|
|
421
|
+
repoPath: z.string(),
|
|
422
|
+
board: runtimeBoardDataSchema,
|
|
423
|
+
revision: z.number(),
|
|
424
|
+
projectConfig: runtimeProjectConfigSchema
|
|
425
|
+
});
|
|
426
|
+
runtimeWorkspaceStateSaveRequestSchema = z.object({
|
|
427
|
+
board: runtimeBoardDataSchema,
|
|
428
|
+
revision: z.number()
|
|
429
|
+
});
|
|
430
|
+
runtimeVisualElementSchema = z.object({
|
|
431
|
+
elementSelector: z.string().optional(),
|
|
432
|
+
elementText: z.string().optional(),
|
|
433
|
+
componentName: z.string().optional(),
|
|
434
|
+
componentChain: z.array(z.string()).optional(),
|
|
435
|
+
sourceFile: z.string().optional(),
|
|
436
|
+
sourceLine: z.number().optional(),
|
|
437
|
+
// The page the element was captured on. Selections can span pages, so this is
|
|
438
|
+
// per-element rather than relying on the visualComment-level pageUrl.
|
|
439
|
+
pageUrl: z.string().optional()
|
|
440
|
+
});
|
|
441
|
+
runtimeVisualCommentSchema = z.object({
|
|
442
|
+
pageUrl: z.string().optional(),
|
|
443
|
+
elements: z.array(runtimeVisualElementSchema).default([])
|
|
444
|
+
});
|
|
445
|
+
runtimeCardCreateRequestSchema = z.object({
|
|
446
|
+
description: z.string(),
|
|
447
|
+
type: cardTypeSchema.optional(),
|
|
448
|
+
// Browser-extension element references; folded into the description server-side.
|
|
449
|
+
visualComment: runtimeVisualCommentSchema.optional(),
|
|
450
|
+
agentId: runtimeAgentIdSchema.optional(),
|
|
451
|
+
priority: runtimeCardPrioritySchema.optional(),
|
|
452
|
+
readyForDev: z.boolean().optional(),
|
|
453
|
+
dependsOn: z.string().optional(),
|
|
454
|
+
waitsFor: z.array(z.string()).optional(),
|
|
455
|
+
subtaskIds: z.array(z.string()).optional(),
|
|
456
|
+
columnId: runtimeBoardColumnIdSchema.optional(),
|
|
457
|
+
baseRef: z.string().optional(),
|
|
458
|
+
githubIssueUrl: z.string().optional(),
|
|
459
|
+
workflowId: z.string().optional(),
|
|
460
|
+
descriptionAttachments: z.array(reviewAttachmentSchema).optional(),
|
|
461
|
+
branchName: z.string().optional(),
|
|
462
|
+
// Optional per-ticket overrides edited before creation. When omitted, the card
|
|
463
|
+
// snapshots the resolved workflow's pairs and defaults the active level to the
|
|
464
|
+
// workflow's highest configured tier.
|
|
465
|
+
modelConfig: cardModelConfigSchema.optional(),
|
|
466
|
+
activeLevel: tierLevelSchema.optional()
|
|
467
|
+
});
|
|
468
|
+
runtimeCardMoveRequestSchema = z.object({
|
|
469
|
+
cardId: z.string(),
|
|
470
|
+
targetColumnId: runtimeBoardColumnIdSchema,
|
|
471
|
+
targetIndex: z.number().int().nonnegative().optional(),
|
|
472
|
+
revision: z.number()
|
|
473
|
+
});
|
|
474
|
+
runtimeCardUpdateRequestSchema = z.object({
|
|
475
|
+
cardId: z.string(),
|
|
476
|
+
description: z.string().optional(),
|
|
477
|
+
descriptionAttachments: z.array(reviewAttachmentSchema).optional(),
|
|
478
|
+
type: cardTypeSchema.optional(),
|
|
479
|
+
agentId: runtimeAgentIdSchema.optional(),
|
|
480
|
+
priority: runtimeCardPrioritySchema.optional(),
|
|
481
|
+
readyForDev: z.boolean().optional(),
|
|
482
|
+
dependsOn: z.string().optional(),
|
|
483
|
+
waitsFor: z.array(z.string()).optional(),
|
|
484
|
+
subtaskIds: z.array(z.string()).optional(),
|
|
485
|
+
workflowId: z.string().optional(),
|
|
486
|
+
branchName: z.string().optional(),
|
|
487
|
+
plan: z.string().optional(),
|
|
488
|
+
activeLevel: tierLevelSchema.optional(),
|
|
489
|
+
modelConfig: cardModelConfigSchema.optional(),
|
|
490
|
+
revision: z.number()
|
|
491
|
+
});
|
|
492
|
+
memoryScopeSchema = z.enum(["global", "project"]);
|
|
493
|
+
memoryTypeSchema = z.enum([
|
|
494
|
+
"fact",
|
|
495
|
+
"convention",
|
|
496
|
+
"decision",
|
|
497
|
+
"preference",
|
|
498
|
+
"rule",
|
|
499
|
+
"lesson",
|
|
500
|
+
"sharp_edge"
|
|
501
|
+
]);
|
|
502
|
+
memorySourceTypeSchema = z.enum(["user_correction", "explicit_save", "task_lesson", "manual_human"]);
|
|
503
|
+
memoryStatusSchema = z.enum(["pending", "approved"]);
|
|
504
|
+
runtimeMemoryOriginAgentSchema = z.object({
|
|
505
|
+
agent: z.string(),
|
|
506
|
+
model: z.string().optional()
|
|
507
|
+
});
|
|
508
|
+
runtimeMemorySchema = z.object({
|
|
509
|
+
id: z.string(),
|
|
510
|
+
scope: memoryScopeSchema,
|
|
511
|
+
workspaceId: z.string().nullable(),
|
|
512
|
+
originWorkspaceId: z.string().nullable().optional(),
|
|
513
|
+
type: memoryTypeSchema,
|
|
514
|
+
title: z.string(),
|
|
515
|
+
content: z.string(),
|
|
516
|
+
sourceType: memorySourceTypeSchema,
|
|
517
|
+
importance: z.number().int().min(1).max(3).default(1),
|
|
518
|
+
tags: z.array(z.string()).default([]),
|
|
519
|
+
boundWorkspaceIds: z.array(z.string()).default([]),
|
|
520
|
+
originCardId: z.string().nullable().optional(),
|
|
521
|
+
originAgent: runtimeMemoryOriginAgentSchema.nullable().optional(),
|
|
522
|
+
status: memoryStatusSchema.default("approved"),
|
|
523
|
+
createdAt: z.number(),
|
|
524
|
+
updatedAt: z.number()
|
|
525
|
+
});
|
|
526
|
+
recurringScheduleKindSchema = z.enum(["interval", "calendar"]);
|
|
527
|
+
recurringScheduleSchema = z.discriminatedUnion("kind", [
|
|
528
|
+
z.object({ kind: z.literal("interval"), intervalSeconds: z.number().int().positive() }),
|
|
529
|
+
z.object({ kind: z.literal("calendar"), cronExpr: z.string().min(1), timezone: z.string().min(1) })
|
|
530
|
+
]);
|
|
531
|
+
recurringRunStatusSchema = z.enum(["running", "ok", "error", "killed"]);
|
|
532
|
+
recurringRunTriggerSchema = z.enum(["schedule", "manual"]);
|
|
533
|
+
recurringAgentRunSchema = z.object({
|
|
534
|
+
id: z.string(),
|
|
535
|
+
startedAt: z.number(),
|
|
536
|
+
endedAt: z.number().optional(),
|
|
537
|
+
status: recurringRunStatusSchema,
|
|
538
|
+
summary: z.string().optional(),
|
|
539
|
+
tokens: z.number().optional(),
|
|
540
|
+
trigger: recurringRunTriggerSchema.default("schedule"),
|
|
541
|
+
streamId: z.string().optional()
|
|
542
|
+
});
|
|
543
|
+
recurringAgentSchema = z.object({
|
|
544
|
+
id: z.string(),
|
|
545
|
+
name: z.string(),
|
|
546
|
+
instructions: z.string().default(""),
|
|
547
|
+
schedule: recurringScheduleSchema,
|
|
548
|
+
model: agentModelChoiceSchema,
|
|
549
|
+
enabled: z.boolean().default(true),
|
|
550
|
+
lastRunAt: z.number().optional(),
|
|
551
|
+
nextRunAt: z.number().optional(),
|
|
552
|
+
journal: z.string().default(""),
|
|
553
|
+
createdAt: z.number(),
|
|
554
|
+
updatedAt: z.number(),
|
|
555
|
+
recentRuns: z.array(recurringAgentRunSchema).default([])
|
|
556
|
+
});
|
|
557
|
+
recurringAgentCreateRequestSchema = z.object({
|
|
558
|
+
name: z.string().min(1),
|
|
559
|
+
instructions: z.string().optional(),
|
|
560
|
+
schedule: recurringScheduleSchema,
|
|
561
|
+
model: agentModelChoiceSchema.optional(),
|
|
562
|
+
enabled: z.boolean().optional()
|
|
563
|
+
});
|
|
564
|
+
recurringAgentUpdateRequestSchema = z.object({
|
|
565
|
+
id: z.string(),
|
|
566
|
+
name: z.string().min(1).optional(),
|
|
567
|
+
instructions: z.string().optional(),
|
|
568
|
+
schedule: recurringScheduleSchema.optional(),
|
|
569
|
+
model: agentModelChoiceSchema.optional(),
|
|
570
|
+
enabled: z.boolean().optional(),
|
|
571
|
+
journal: z.string().optional()
|
|
572
|
+
});
|
|
573
|
+
projectFolderSchema = z.object({
|
|
574
|
+
id: z.string(),
|
|
575
|
+
name: z.string(),
|
|
576
|
+
collapsed: z.boolean().default(false),
|
|
577
|
+
projectIds: z.array(z.string())
|
|
578
|
+
});
|
|
579
|
+
topLevelItemSchema = z.discriminatedUnion("type", [
|
|
580
|
+
z.object({ type: z.literal("folder"), id: z.string() }),
|
|
581
|
+
z.object({ type: z.literal("project"), workspaceId: z.string() })
|
|
582
|
+
]);
|
|
583
|
+
projectsLayoutSchema = z.object({
|
|
584
|
+
version: z.literal(1),
|
|
585
|
+
topLevel: z.array(topLevelItemSchema),
|
|
586
|
+
folders: z.record(z.string(), projectFolderSchema)
|
|
587
|
+
});
|
|
588
|
+
runtimeProjectSchema = z.object({
|
|
589
|
+
workspaceId: z.string(),
|
|
590
|
+
repoPath: z.string(),
|
|
591
|
+
name: z.string(),
|
|
592
|
+
lastUpdated: z.number()
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// src/config/paths.ts
|
|
598
|
+
import { homedir } from "node:os";
|
|
599
|
+
import { join } from "node:path";
|
|
600
|
+
var WHIPPED_HOME_DIR;
|
|
601
|
+
var init_paths = __esm({
|
|
602
|
+
"src/config/paths.ts"() {
|
|
603
|
+
"use strict";
|
|
604
|
+
WHIPPED_HOME_DIR = process.env.WHIPPED_HOME_DIR ?? join(homedir(), ".whipped");
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// src/core/logger.ts
|
|
609
|
+
import { mkdirSync } from "node:fs";
|
|
610
|
+
import { join as join2 } from "node:path";
|
|
611
|
+
import pino from "pino";
|
|
612
|
+
var LOGS_DIR, date, streams, logger;
|
|
613
|
+
var init_logger = __esm({
|
|
614
|
+
"src/core/logger.ts"() {
|
|
615
|
+
"use strict";
|
|
616
|
+
init_paths();
|
|
617
|
+
LOGS_DIR = join2(WHIPPED_HOME_DIR, "logs");
|
|
618
|
+
date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
619
|
+
streams = [
|
|
620
|
+
{
|
|
621
|
+
stream: pino.transport({
|
|
622
|
+
target: "pino-pretty",
|
|
623
|
+
options: {
|
|
624
|
+
colorize: true,
|
|
625
|
+
translateTime: "HH:MM:ss",
|
|
626
|
+
ignore: "pid,hostname",
|
|
627
|
+
messageFormat: "{msg}",
|
|
628
|
+
errorLikeObjectKeys: ["err"]
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
}
|
|
632
|
+
];
|
|
633
|
+
try {
|
|
634
|
+
mkdirSync(LOGS_DIR, { recursive: true });
|
|
635
|
+
streams.push({
|
|
636
|
+
stream: pino.destination({
|
|
637
|
+
dest: join2(LOGS_DIR, `whipped-${date}.log`),
|
|
638
|
+
sync: false
|
|
639
|
+
})
|
|
640
|
+
});
|
|
641
|
+
} catch {
|
|
642
|
+
}
|
|
643
|
+
logger = pino(
|
|
644
|
+
{
|
|
645
|
+
level: "debug",
|
|
646
|
+
base: null,
|
|
647
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
648
|
+
formatters: {
|
|
649
|
+
level: (label) => ({ level: label })
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
pino.multistream(streams)
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// src/state/secrets-crypto.ts
|
|
658
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
659
|
+
import { chmodSync, existsSync, mkdirSync as mkdirSync2, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
660
|
+
import { dirname, join as join3 } from "node:path";
|
|
661
|
+
function generateAndWriteKey() {
|
|
662
|
+
const key = randomBytes(KEY_LENGTH);
|
|
663
|
+
mkdirSync2(dirname(KEY_PATH), { recursive: true });
|
|
664
|
+
writeFileSync(KEY_PATH, key.toString("base64"), { mode: 384 });
|
|
665
|
+
chmodSync(KEY_PATH, 384);
|
|
666
|
+
logger.warn({ path: KEY_PATH }, "Generated new secret key \u2014 any previously-encrypted secrets are now unreadable");
|
|
667
|
+
return key;
|
|
668
|
+
}
|
|
669
|
+
function loadOrGenerateKey() {
|
|
670
|
+
if (cachedKey) return cachedKey;
|
|
671
|
+
if (existsSync(KEY_PATH)) {
|
|
672
|
+
try {
|
|
673
|
+
const raw = readFileSync(KEY_PATH, "utf-8").trim();
|
|
674
|
+
const key = Buffer.from(raw, "base64");
|
|
675
|
+
if (key.length === KEY_LENGTH) {
|
|
676
|
+
cachedKey = key;
|
|
677
|
+
return key;
|
|
678
|
+
}
|
|
679
|
+
logger.error(
|
|
680
|
+
{ path: KEY_PATH, gotLength: key.length, expectedLength: KEY_LENGTH },
|
|
681
|
+
"Secret key file has wrong length; regenerating"
|
|
682
|
+
);
|
|
683
|
+
unlinkSync(KEY_PATH);
|
|
684
|
+
} catch (err) {
|
|
685
|
+
logger.error({ err, path: KEY_PATH }, "Secret key file unreadable; regenerating");
|
|
686
|
+
try {
|
|
687
|
+
unlinkSync(KEY_PATH);
|
|
688
|
+
} catch {
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
cachedKey = generateAndWriteKey();
|
|
693
|
+
return cachedKey;
|
|
694
|
+
}
|
|
695
|
+
function encrypt(plaintext) {
|
|
696
|
+
const key = loadOrGenerateKey();
|
|
697
|
+
const nonce = randomBytes(NONCE_LENGTH);
|
|
698
|
+
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
699
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
700
|
+
const tag = cipher.getAuthTag();
|
|
701
|
+
return PREFIX + Buffer.concat([nonce, ciphertext, tag]).toString("base64");
|
|
702
|
+
}
|
|
703
|
+
function decrypt(value) {
|
|
704
|
+
try {
|
|
705
|
+
const key = loadOrGenerateKey();
|
|
706
|
+
const combined = Buffer.from(value.slice(PREFIX.length), "base64");
|
|
707
|
+
if (combined.length < NONCE_LENGTH + TAG_LENGTH) {
|
|
708
|
+
throw new Error(`ciphertext too short (${combined.length} bytes)`);
|
|
709
|
+
}
|
|
710
|
+
const nonce = combined.subarray(0, NONCE_LENGTH);
|
|
711
|
+
const tag = combined.subarray(combined.length - TAG_LENGTH);
|
|
712
|
+
const ciphertext = combined.subarray(NONCE_LENGTH, combined.length - TAG_LENGTH);
|
|
713
|
+
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
714
|
+
decipher.setAuthTag(tag);
|
|
715
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
|
|
716
|
+
} catch (err) {
|
|
717
|
+
logger.error(
|
|
718
|
+
{ err: err.message },
|
|
719
|
+
"Failed to decrypt secret \u2014 returning empty. Update the value via the UI to re-encrypt with the current key."
|
|
720
|
+
);
|
|
721
|
+
return "";
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
var KEY_PATH, KEY_LENGTH, NONCE_LENGTH, TAG_LENGTH, PREFIX, cachedKey;
|
|
725
|
+
var init_secrets_crypto = __esm({
|
|
726
|
+
"src/state/secrets-crypto.ts"() {
|
|
727
|
+
"use strict";
|
|
728
|
+
init_paths();
|
|
729
|
+
init_logger();
|
|
730
|
+
KEY_PATH = join3(WHIPPED_HOME_DIR, ".secret_key");
|
|
731
|
+
KEY_LENGTH = 32;
|
|
732
|
+
NONCE_LENGTH = 12;
|
|
733
|
+
TAG_LENGTH = 16;
|
|
734
|
+
PREFIX = "enc:v1:";
|
|
735
|
+
cachedKey = null;
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// src/state/db.ts
|
|
740
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
741
|
+
import { fileURLToPath } from "node:url";
|
|
742
|
+
import Database from "better-sqlite3";
|
|
743
|
+
function getDb() {
|
|
744
|
+
if (!cachedDb) {
|
|
745
|
+
throw new Error("Database not initialised. Call openDb() at startup before getDb().");
|
|
746
|
+
}
|
|
747
|
+
return cachedDb;
|
|
748
|
+
}
|
|
749
|
+
var MIGRATIONS_DIR, DEFAULT_DB_PATH, cachedDb;
|
|
750
|
+
var init_db = __esm({
|
|
751
|
+
"src/state/db.ts"() {
|
|
752
|
+
"use strict";
|
|
753
|
+
init_paths();
|
|
754
|
+
init_logger();
|
|
755
|
+
init_secrets_crypto();
|
|
756
|
+
MIGRATIONS_DIR = join4(dirname2(fileURLToPath(import.meta.url)), "migrations");
|
|
757
|
+
DEFAULT_DB_PATH = join4(WHIPPED_HOME_DIR, "whipped.db");
|
|
758
|
+
cachedDb = null;
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// src/config/runtime-config.ts
|
|
763
|
+
import { join as join5 } from "node:path";
|
|
764
|
+
var WORKSPACES_DIR, ATTACHMENTS_DIR;
|
|
765
|
+
var init_runtime_config = __esm({
|
|
766
|
+
"src/config/runtime-config.ts"() {
|
|
767
|
+
"use strict";
|
|
768
|
+
init_api_contract();
|
|
769
|
+
init_db();
|
|
770
|
+
init_secrets_crypto();
|
|
771
|
+
init_paths();
|
|
772
|
+
WORKSPACES_DIR = join5(WHIPPED_HOME_DIR, "workspaces");
|
|
773
|
+
ATTACHMENTS_DIR = join5(WHIPPED_HOME_DIR, "attachments");
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
// src/core/task-id.ts
|
|
778
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
779
|
+
function generateTaskId() {
|
|
780
|
+
return randomBytes2(8).toString("hex");
|
|
781
|
+
}
|
|
782
|
+
var init_task_id = __esm({
|
|
783
|
+
"src/core/task-id.ts"() {
|
|
784
|
+
"use strict";
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
// src/state/workspace-state.ts
|
|
789
|
+
var workspace_state_exports = {};
|
|
790
|
+
__export(workspace_state_exports, {
|
|
791
|
+
appendActivityLog: () => appendActivityLog,
|
|
792
|
+
appendTerminalSession: () => appendTerminalSession,
|
|
793
|
+
clearCardSession: () => clearCardSession,
|
|
794
|
+
closeAllOpenTerminalSessions: () => closeAllOpenTerminalSessions,
|
|
795
|
+
createCard: () => createCard,
|
|
796
|
+
deleteCard: () => deleteCard,
|
|
797
|
+
downloadGithubImages: () => downloadGithubImages,
|
|
798
|
+
endTerminalSession: () => endTerminalSession,
|
|
799
|
+
extractSignedImageUrls: () => extractSignedImageUrls,
|
|
800
|
+
linkCommentToSession: () => linkCommentToSession,
|
|
801
|
+
listWorkspaces: () => listWorkspaces,
|
|
802
|
+
loadBoard: () => loadBoard,
|
|
803
|
+
loadProjectConfig: () => loadProjectConfig,
|
|
804
|
+
loadTerminalBuffer: () => loadTerminalBuffer,
|
|
805
|
+
loadWorkspaceContext: () => loadWorkspaceContext,
|
|
806
|
+
loadWorkspaceState: () => loadWorkspaceState,
|
|
807
|
+
moveCard: () => moveCard,
|
|
808
|
+
removeWorkspace: () => removeWorkspace,
|
|
809
|
+
saveAttachment: () => saveAttachment,
|
|
810
|
+
saveProjectConfig: () => saveProjectConfig,
|
|
811
|
+
saveTerminalBuffer: () => saveTerminalBuffer,
|
|
812
|
+
saveWorkspaceState: () => saveWorkspaceState,
|
|
813
|
+
stampReviewCommentMetadata: () => stampReviewCommentMetadata,
|
|
814
|
+
updateCard: () => updateCard,
|
|
815
|
+
updateProjectConfig: () => updateProjectConfig
|
|
816
|
+
});
|
|
817
|
+
import { createHash, randomBytes as randomBytes3 } from "node:crypto";
|
|
818
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
819
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
820
|
+
import { join as join6 } from "node:path";
|
|
821
|
+
function workspaceDirPath(workspaceId2) {
|
|
822
|
+
return join6(WORKSPACES_DIR, workspaceId2);
|
|
823
|
+
}
|
|
824
|
+
function bufferFilePath(workspaceId2, streamId) {
|
|
825
|
+
const safe = streamId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
826
|
+
return join6(workspaceDirPath(workspaceId2), "buffers", `${safe}.ansi`);
|
|
827
|
+
}
|
|
828
|
+
function safeJsonParse(raw, fallback) {
|
|
829
|
+
try {
|
|
830
|
+
return JSON.parse(raw);
|
|
831
|
+
} catch {
|
|
832
|
+
return fallback;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
function cardFromRow(row, children) {
|
|
836
|
+
const pr = row.pr_json ? safeJsonParse(row.pr_json, void 0) : void 0;
|
|
837
|
+
const card = {
|
|
838
|
+
id: row.id,
|
|
839
|
+
description: row.description,
|
|
840
|
+
descriptionAttachments: safeJsonParse(row.description_attachments_json, []),
|
|
841
|
+
columnId: row.column_id,
|
|
842
|
+
type: row.type,
|
|
843
|
+
readyForDev: row.ready_for_dev === 1,
|
|
844
|
+
waitsFor: children.waitsFor,
|
|
845
|
+
subtaskIds: children.subtaskIds,
|
|
846
|
+
autoFixAttempts: row.auto_fix_attempts,
|
|
847
|
+
activeLevel: row.active_level ?? "medium",
|
|
848
|
+
baseRef: row.base_ref,
|
|
849
|
+
createdAt: row.created_at,
|
|
850
|
+
updatedAt: row.updated_at,
|
|
851
|
+
githubCommentIds: safeJsonParse(row.github_comment_ids_json, []),
|
|
852
|
+
reviewComments: children.reviewComments,
|
|
853
|
+
activityLog: children.activityLog,
|
|
854
|
+
terminalSessions: children.terminalSessions
|
|
855
|
+
};
|
|
856
|
+
if (row.agent_id) card.agentId = row.agent_id;
|
|
857
|
+
if (row.priority) card.priority = row.priority;
|
|
858
|
+
if (row.workflow_id) card.workflowId = row.workflow_id;
|
|
859
|
+
if (row.github_issue_url) card.githubIssueUrl = row.github_issue_url;
|
|
860
|
+
if (pr) card.pr = pr;
|
|
861
|
+
if (row.worktree_path) card.worktreePath = row.worktree_path;
|
|
862
|
+
if (row.branch_name) card.branchName = row.branch_name;
|
|
863
|
+
if (row.depends_on_id) card.dependsOn = row.depends_on_id;
|
|
864
|
+
if (row.slack_message_ts) card.slackMessageTs = row.slack_message_ts;
|
|
865
|
+
if (row.slack_channel_id) card.slackChannelId = row.slack_channel_id;
|
|
866
|
+
if (row.plan) card.plan = row.plan;
|
|
867
|
+
if (row.model_config_json) {
|
|
868
|
+
card.modelConfig = safeJsonParse(row.model_config_json, void 0);
|
|
869
|
+
}
|
|
870
|
+
return card;
|
|
871
|
+
}
|
|
872
|
+
function loadCardChildren(db, cardId) {
|
|
873
|
+
const waitsFor = db.prepare("SELECT waits_for_id FROM card_waits_for WHERE card_id = ?").all(cardId).map((r) => r.waits_for_id);
|
|
874
|
+
const subtaskIds = db.prepare("SELECT subtask_id FROM card_subtasks WHERE story_id = ?").all(cardId).map((r) => r.subtask_id);
|
|
875
|
+
const activityLog = db.prepare("SELECT timestamp, message FROM activity_log WHERE card_id = ? ORDER BY timestamp, id").all(cardId).map((r) => ({ timestamp: r.timestamp, message: r.message }));
|
|
876
|
+
const reviewRows = db.prepare(
|
|
877
|
+
"SELECT comment_id, created_at, type, actor_type, actor_id, actor_source, status, stream_id, summary, issues_json, attachments_json, metadata_json FROM review_comments WHERE card_id = ? ORDER BY created_at"
|
|
878
|
+
).all(cardId);
|
|
879
|
+
const reviewComments = reviewRows.map((r) => {
|
|
880
|
+
const comment = {
|
|
881
|
+
id: r.comment_id,
|
|
882
|
+
type: r.type,
|
|
883
|
+
actor: {
|
|
884
|
+
type: r.actor_type,
|
|
885
|
+
id: r.actor_id,
|
|
886
|
+
...r.actor_source ? { source: r.actor_source } : {}
|
|
887
|
+
},
|
|
888
|
+
createdAt: r.created_at,
|
|
889
|
+
summary: r.summary
|
|
890
|
+
};
|
|
891
|
+
if (r.status) comment.status = r.status;
|
|
892
|
+
if (r.stream_id) comment.streamId = r.stream_id;
|
|
893
|
+
const issues = safeJsonParse(r.issues_json, []);
|
|
894
|
+
if (issues && issues.length > 0) comment.issues = issues;
|
|
895
|
+
const attachments = safeJsonParse(r.attachments_json, []);
|
|
896
|
+
if (attachments && attachments.length > 0) comment.attachments = attachments;
|
|
897
|
+
const metadata = safeJsonParse(r.metadata_json, {});
|
|
898
|
+
if (metadata && Object.keys(metadata).length > 0) comment.metadata = metadata;
|
|
899
|
+
return comment;
|
|
900
|
+
});
|
|
901
|
+
const sessionRows = db.prepare(
|
|
902
|
+
"SELECT stream_id, type, started_at, ended_at, agent_id, state FROM terminal_sessions WHERE card_id = ? ORDER BY started_at"
|
|
903
|
+
).all(cardId);
|
|
904
|
+
const terminalSessions = sessionRows.map((r) => {
|
|
905
|
+
const entry = {
|
|
906
|
+
streamId: r.stream_id,
|
|
907
|
+
type: r.type,
|
|
908
|
+
startedAt: r.started_at
|
|
909
|
+
};
|
|
910
|
+
if (r.ended_at != null) entry.endedAt = r.ended_at;
|
|
911
|
+
if (r.agent_id) entry.agentId = r.agent_id;
|
|
912
|
+
if (r.state) entry.state = r.state;
|
|
913
|
+
return entry;
|
|
914
|
+
});
|
|
915
|
+
return { waitsFor, subtaskIds, activityLog, reviewComments, terminalSessions };
|
|
916
|
+
}
|
|
917
|
+
function upsertCardRow(db, workspaceId2, card, columnPosition) {
|
|
918
|
+
db.prepare(
|
|
919
|
+
`INSERT INTO cards (
|
|
920
|
+
id, workspace_id, description, description_attachments_json,
|
|
921
|
+
column_id, column_position, type, ready_for_dev,
|
|
922
|
+
agent_id, priority, auto_fix_attempts, base_ref, workflow_id,
|
|
923
|
+
github_issue_url, pr_json, github_comment_ids_json,
|
|
924
|
+
worktree_path, branch_name, depends_on_id,
|
|
925
|
+
slack_message_ts, slack_channel_id,
|
|
926
|
+
plan, active_level, model_config_json, created_at, updated_at
|
|
927
|
+
) VALUES (
|
|
928
|
+
?, ?, ?, ?,
|
|
929
|
+
?, ?, ?, ?,
|
|
930
|
+
?, ?, ?, ?, ?,
|
|
931
|
+
?, ?, ?,
|
|
932
|
+
?, ?, ?,
|
|
933
|
+
?, ?,
|
|
934
|
+
?, ?, ?, ?, ?
|
|
935
|
+
) ON CONFLICT(id) DO UPDATE SET
|
|
936
|
+
description = excluded.description,
|
|
937
|
+
description_attachments_json = excluded.description_attachments_json,
|
|
938
|
+
column_id = excluded.column_id,
|
|
939
|
+
column_position = excluded.column_position,
|
|
940
|
+
type = excluded.type,
|
|
941
|
+
ready_for_dev = excluded.ready_for_dev,
|
|
942
|
+
agent_id = excluded.agent_id,
|
|
943
|
+
priority = excluded.priority,
|
|
944
|
+
auto_fix_attempts = excluded.auto_fix_attempts,
|
|
945
|
+
base_ref = excluded.base_ref,
|
|
946
|
+
workflow_id = excluded.workflow_id,
|
|
947
|
+
github_issue_url = excluded.github_issue_url,
|
|
948
|
+
pr_json = excluded.pr_json,
|
|
949
|
+
github_comment_ids_json = excluded.github_comment_ids_json,
|
|
950
|
+
worktree_path = excluded.worktree_path,
|
|
951
|
+
branch_name = excluded.branch_name,
|
|
952
|
+
depends_on_id = excluded.depends_on_id,
|
|
953
|
+
slack_message_ts = excluded.slack_message_ts,
|
|
954
|
+
slack_channel_id = excluded.slack_channel_id,
|
|
955
|
+
plan = excluded.plan,
|
|
956
|
+
active_level = excluded.active_level,
|
|
957
|
+
model_config_json = excluded.model_config_json,
|
|
958
|
+
updated_at = excluded.updated_at`
|
|
959
|
+
).run(
|
|
960
|
+
card.id,
|
|
961
|
+
workspaceId2,
|
|
962
|
+
card.description,
|
|
963
|
+
JSON.stringify(card.descriptionAttachments ?? []),
|
|
964
|
+
card.columnId,
|
|
965
|
+
columnPosition,
|
|
966
|
+
card.type,
|
|
967
|
+
card.readyForDev ? 1 : 0,
|
|
968
|
+
card.agentId ?? null,
|
|
969
|
+
card.priority ?? null,
|
|
970
|
+
card.autoFixAttempts,
|
|
971
|
+
card.baseRef,
|
|
972
|
+
card.workflowId ?? null,
|
|
973
|
+
card.githubIssueUrl ?? null,
|
|
974
|
+
card.pr ? JSON.stringify(card.pr) : null,
|
|
975
|
+
JSON.stringify(card.githubCommentIds ?? []),
|
|
976
|
+
card.worktreePath ?? null,
|
|
977
|
+
card.branchName ?? null,
|
|
978
|
+
card.dependsOn ?? null,
|
|
979
|
+
card.slackMessageTs ?? null,
|
|
980
|
+
card.slackChannelId ?? null,
|
|
981
|
+
card.plan ?? null,
|
|
982
|
+
card.activeLevel ?? "medium",
|
|
983
|
+
card.modelConfig ? JSON.stringify(card.modelConfig) : null,
|
|
984
|
+
card.createdAt,
|
|
985
|
+
card.updatedAt
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
function replaceCardWaitsFor(db, cardId, waitsFor) {
|
|
989
|
+
db.prepare("DELETE FROM card_waits_for WHERE card_id = ?").run(cardId);
|
|
990
|
+
const insert = db.prepare(INSERT_CARD_WAITS_FOR_IF_EXISTS);
|
|
991
|
+
for (const targetId of waitsFor) insert.run(cardId, targetId, targetId, cardId, targetId);
|
|
992
|
+
}
|
|
993
|
+
function replaceCardSubtasks(db, storyId, subtaskIds) {
|
|
994
|
+
db.prepare("DELETE FROM card_subtasks WHERE story_id = ?").run(storyId);
|
|
995
|
+
const insert = db.prepare(INSERT_CARD_SUBTASK_IF_EXISTS);
|
|
996
|
+
for (const subtaskId of subtaskIds) insert.run(storyId, subtaskId, subtaskId, storyId, subtaskId);
|
|
997
|
+
}
|
|
998
|
+
function replaceCardChildren(db, card) {
|
|
999
|
+
replaceCardWaitsFor(db, card.id, card.waitsFor ?? []);
|
|
1000
|
+
replaceCardSubtasks(db, card.id, card.subtaskIds ?? []);
|
|
1001
|
+
db.prepare("DELETE FROM activity_log WHERE card_id = ?").run(card.id);
|
|
1002
|
+
const insertActivity = db.prepare("INSERT INTO activity_log (card_id, timestamp, message) VALUES (?, ?, ?)");
|
|
1003
|
+
for (const entry of card.activityLog ?? []) insertActivity.run(card.id, entry.timestamp, entry.message);
|
|
1004
|
+
db.prepare("DELETE FROM review_comments WHERE card_id = ?").run(card.id);
|
|
1005
|
+
const insertReview = db.prepare(
|
|
1006
|
+
`INSERT INTO review_comments (
|
|
1007
|
+
comment_id, card_id, created_at, type, actor_type, actor_id, actor_source,
|
|
1008
|
+
status, stream_id, summary, issues_json, attachments_json, metadata_json
|
|
1009
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1010
|
+
);
|
|
1011
|
+
for (const c of card.reviewComments ?? []) {
|
|
1012
|
+
insertReview.run(
|
|
1013
|
+
c.id,
|
|
1014
|
+
card.id,
|
|
1015
|
+
c.createdAt,
|
|
1016
|
+
c.type,
|
|
1017
|
+
c.actor.type,
|
|
1018
|
+
c.actor.id,
|
|
1019
|
+
c.actor.source ?? null,
|
|
1020
|
+
c.status ?? null,
|
|
1021
|
+
c.streamId ?? null,
|
|
1022
|
+
c.summary,
|
|
1023
|
+
JSON.stringify(c.issues ?? []),
|
|
1024
|
+
JSON.stringify(c.attachments ?? []),
|
|
1025
|
+
JSON.stringify(c.metadata ?? {})
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
db.prepare("DELETE FROM terminal_sessions WHERE card_id = ?").run(card.id);
|
|
1029
|
+
const insertSession = db.prepare(
|
|
1030
|
+
`INSERT INTO terminal_sessions (card_id, stream_id, type, started_at, ended_at, agent_id, state)
|
|
1031
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
1032
|
+
);
|
|
1033
|
+
for (const s of card.terminalSessions ?? []) {
|
|
1034
|
+
insertSession.run(card.id, s.streamId, s.type, s.startedAt, s.endedAt ?? null, s.agentId ?? null, s.state ?? null);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function bumpBoardRevision(db, workspaceId2) {
|
|
1038
|
+
const row = db.prepare("UPDATE workspaces SET board_revision = board_revision + 1 WHERE id = ? RETURNING board_revision").get(workspaceId2);
|
|
1039
|
+
return row?.board_revision ?? 0;
|
|
1040
|
+
}
|
|
1041
|
+
function loadBoardInternal(workspaceId2) {
|
|
1042
|
+
const db = getDb();
|
|
1043
|
+
const cardRows = db.prepare("SELECT * FROM cards WHERE workspace_id = ? ORDER BY column_id, column_position").all(workspaceId2);
|
|
1044
|
+
const taskIdsByColumn = {
|
|
1045
|
+
todo: [],
|
|
1046
|
+
in_progress: [],
|
|
1047
|
+
reopened: [],
|
|
1048
|
+
ready_for_review: [],
|
|
1049
|
+
blocked: [],
|
|
1050
|
+
done: []
|
|
1051
|
+
};
|
|
1052
|
+
const cards = {};
|
|
1053
|
+
for (const row of cardRows) {
|
|
1054
|
+
const card = cardFromRow(row, loadCardChildren(db, row.id));
|
|
1055
|
+
cards[card.id] = card;
|
|
1056
|
+
taskIdsByColumn[row.column_id].push(row.id);
|
|
1057
|
+
}
|
|
1058
|
+
const columns = BOARD_COLUMNS.map((col) => ({
|
|
1059
|
+
id: col.id,
|
|
1060
|
+
title: col.title,
|
|
1061
|
+
taskIds: taskIdsByColumn[col.id]
|
|
1062
|
+
}));
|
|
1063
|
+
return { columns, cards };
|
|
1064
|
+
}
|
|
1065
|
+
function saveBoardInternal(workspaceId2, board) {
|
|
1066
|
+
const db = getDb();
|
|
1067
|
+
db.pragma("defer_foreign_keys = ON");
|
|
1068
|
+
const positionFor = /* @__PURE__ */ new Map();
|
|
1069
|
+
for (const col of board.columns) {
|
|
1070
|
+
col.taskIds.forEach((cardId, idx) => {
|
|
1071
|
+
positionFor.set(cardId, { columnId: col.id, position: idx });
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
const existing = db.prepare("SELECT id FROM cards WHERE workspace_id = ?").all(workspaceId2);
|
|
1075
|
+
const requestIds = new Set(Object.keys(board.cards));
|
|
1076
|
+
const toDelete = existing.filter((r) => !requestIds.has(r.id)).map((r) => r.id);
|
|
1077
|
+
if (toDelete.length > 0) {
|
|
1078
|
+
const placeholders = toDelete.map(() => "?").join(",");
|
|
1079
|
+
db.prepare(`DELETE FROM cards WHERE id IN (${placeholders})`).run(...toDelete);
|
|
1080
|
+
}
|
|
1081
|
+
for (const card of Object.values(board.cards)) {
|
|
1082
|
+
const pos = positionFor.get(card.id);
|
|
1083
|
+
const column = pos?.columnId ?? card.columnId;
|
|
1084
|
+
const position = pos?.position ?? 0;
|
|
1085
|
+
const sanitizedDependsOn = card.dependsOn && board.cards[card.dependsOn] ? card.dependsOn : void 0;
|
|
1086
|
+
const cardForRow = column === card.columnId && sanitizedDependsOn === card.dependsOn ? card : { ...card, columnId: column, dependsOn: sanitizedDependsOn };
|
|
1087
|
+
upsertCardRow(db, workspaceId2, cardForRow, position);
|
|
1088
|
+
replaceCardChildren(db, cardForRow);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
async function loadBoard(workspaceId2) {
|
|
1092
|
+
return loadBoardInternal(workspaceId2);
|
|
1093
|
+
}
|
|
1094
|
+
function loadProjectConfigInternal(workspaceId2) {
|
|
1095
|
+
const db = getDb();
|
|
1096
|
+
const wsRow = db.prepare("SELECT settings_json FROM workspaces WHERE id = ?").get(workspaceId2);
|
|
1097
|
+
if (!wsRow) return runtimeProjectConfigSchema.parse({});
|
|
1098
|
+
let settings = {};
|
|
1099
|
+
try {
|
|
1100
|
+
const parsed2 = JSON.parse(wsRow.settings_json);
|
|
1101
|
+
if (parsed2 && typeof parsed2 === "object") settings = parsed2;
|
|
1102
|
+
} catch {
|
|
1103
|
+
}
|
|
1104
|
+
const workflowRows = db.prepare("SELECT id, name, is_default, for_story, slots_json FROM workflows WHERE workspace_id = ?").all(workspaceId2);
|
|
1105
|
+
const workflows = workflowRows.map((row) => {
|
|
1106
|
+
let slots = [];
|
|
1107
|
+
try {
|
|
1108
|
+
slots = JSON.parse(row.slots_json);
|
|
1109
|
+
} catch {
|
|
1110
|
+
slots = [];
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
id: row.id,
|
|
1114
|
+
name: row.name,
|
|
1115
|
+
isDefault: row.is_default === 1,
|
|
1116
|
+
forStory: row.for_story === 1,
|
|
1117
|
+
slots
|
|
1118
|
+
};
|
|
1119
|
+
});
|
|
1120
|
+
const secretRows = db.prepare("SELECT key, value FROM workspace_secrets WHERE workspace_id = ?").all(workspaceId2);
|
|
1121
|
+
const secrets = secretRows.map((r) => ({ key: r.key, value: decrypt(r.value) }));
|
|
1122
|
+
const integrationRows = db.prepare("SELECT type, config_json FROM workspace_integrations WHERE workspace_id = ? AND enabled = 1").all(workspaceId2);
|
|
1123
|
+
let github;
|
|
1124
|
+
for (const row of integrationRows) {
|
|
1125
|
+
try {
|
|
1126
|
+
const cfg = JSON.parse(decrypt(row.config_json));
|
|
1127
|
+
if (row.type === "github") github = cfg;
|
|
1128
|
+
} catch {
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
const merged = {
|
|
1132
|
+
...settings,
|
|
1133
|
+
workflows,
|
|
1134
|
+
secrets
|
|
1135
|
+
};
|
|
1136
|
+
if (github) merged.github = github;
|
|
1137
|
+
if (merged.deliveryMode === void 0) {
|
|
1138
|
+
merged.deliveryMode = merged.autoPR === true ? "pr" : "off";
|
|
1139
|
+
}
|
|
1140
|
+
const parsed = runtimeProjectConfigSchema.safeParse(merged);
|
|
1141
|
+
return parsed.success ? parsed.data : runtimeProjectConfigSchema.parse({});
|
|
1142
|
+
}
|
|
1143
|
+
function saveProjectConfigInternal(workspaceId2, config) {
|
|
1144
|
+
const db = getDb();
|
|
1145
|
+
const now = Date.now();
|
|
1146
|
+
const { workflows, secrets, github, ...rest } = config;
|
|
1147
|
+
db.prepare("UPDATE workspaces SET settings_json = ?, updated_at = ? WHERE id = ?").run(
|
|
1148
|
+
JSON.stringify(rest),
|
|
1149
|
+
now,
|
|
1150
|
+
workspaceId2
|
|
1151
|
+
);
|
|
1152
|
+
const workflowsById = /* @__PURE__ */ new Map();
|
|
1153
|
+
for (const wf of workflows) workflowsById.set(wf.id, wf);
|
|
1154
|
+
const workflowsByName = /* @__PURE__ */ new Map();
|
|
1155
|
+
for (const wf of workflowsById.values()) workflowsByName.set(wf.name, wf);
|
|
1156
|
+
db.prepare("DELETE FROM workflows WHERE workspace_id = ?").run(workspaceId2);
|
|
1157
|
+
const insertWf = db.prepare(
|
|
1158
|
+
"INSERT INTO workflows (id, workspace_id, name, is_default, for_story, slots_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
1159
|
+
);
|
|
1160
|
+
for (const wf of workflowsByName.values()) {
|
|
1161
|
+
insertWf.run(
|
|
1162
|
+
wf.id,
|
|
1163
|
+
workspaceId2,
|
|
1164
|
+
wf.name,
|
|
1165
|
+
wf.isDefault ? 1 : 0,
|
|
1166
|
+
wf.forStory ? 1 : 0,
|
|
1167
|
+
JSON.stringify(wf.slots),
|
|
1168
|
+
now,
|
|
1169
|
+
now
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
const secretsByKey = /* @__PURE__ */ new Map();
|
|
1173
|
+
for (const s of secrets) secretsByKey.set(s.key, s);
|
|
1174
|
+
db.prepare("DELETE FROM workspace_secrets WHERE workspace_id = ?").run(workspaceId2);
|
|
1175
|
+
const insertSec = db.prepare("INSERT INTO workspace_secrets (workspace_id, key, value) VALUES (?, ?, ?)");
|
|
1176
|
+
for (const s of secretsByKey.values()) {
|
|
1177
|
+
insertSec.run(workspaceId2, s.key, encrypt(s.value));
|
|
1178
|
+
}
|
|
1179
|
+
db.prepare("DELETE FROM workspace_integrations WHERE workspace_id = ?").run(workspaceId2);
|
|
1180
|
+
const insertInt = db.prepare(
|
|
1181
|
+
"INSERT INTO workspace_integrations (workspace_id, type, enabled, config_json, updated_at) VALUES (?, ?, 1, ?, ?)"
|
|
1182
|
+
);
|
|
1183
|
+
if (github) insertInt.run(workspaceId2, "github", encrypt(JSON.stringify(github)), now);
|
|
1184
|
+
}
|
|
1185
|
+
async function loadProjectConfig(workspaceId2) {
|
|
1186
|
+
return loadProjectConfigInternal(workspaceId2);
|
|
1187
|
+
}
|
|
1188
|
+
async function saveProjectConfig(workspaceId2, config) {
|
|
1189
|
+
const db = getDb();
|
|
1190
|
+
const tx = db.transaction(() => saveProjectConfigInternal(workspaceId2, config));
|
|
1191
|
+
tx();
|
|
1192
|
+
}
|
|
1193
|
+
async function updateProjectConfig(workspaceId2, mutator) {
|
|
1194
|
+
const db = getDb();
|
|
1195
|
+
const tx = db.transaction(() => {
|
|
1196
|
+
const current = loadProjectConfigInternal(workspaceId2);
|
|
1197
|
+
const next = mutator(current);
|
|
1198
|
+
saveProjectConfigInternal(workspaceId2, next);
|
|
1199
|
+
return next;
|
|
1200
|
+
});
|
|
1201
|
+
return tx();
|
|
1202
|
+
}
|
|
1203
|
+
async function loadWorkspaceContext(repoPath) {
|
|
1204
|
+
const db = getDb();
|
|
1205
|
+
const existing = db.prepare("SELECT id, name FROM workspaces WHERE repo_path = ?").get(repoPath);
|
|
1206
|
+
if (existing) {
|
|
1207
|
+
return { workspaceId: existing.id, repoPath, name: existing.name, lastUpdated: 0 };
|
|
1208
|
+
}
|
|
1209
|
+
const workspaceId2 = randomBytes3(4).toString("hex");
|
|
1210
|
+
const name = repoPath.split("/").pop() ?? repoPath;
|
|
1211
|
+
const now = Date.now();
|
|
1212
|
+
db.prepare(
|
|
1213
|
+
"INSERT INTO workspaces (id, repo_path, name, settings_json, created_at, updated_at) VALUES (?, ?, ?, '{}', ?, ?)"
|
|
1214
|
+
).run(workspaceId2, repoPath, name, now, now);
|
|
1215
|
+
return { workspaceId: workspaceId2, repoPath, name, lastUpdated: 0 };
|
|
1216
|
+
}
|
|
1217
|
+
async function listWorkspaces() {
|
|
1218
|
+
const db = getDb();
|
|
1219
|
+
const rows = db.prepare("SELECT id, repo_path, name FROM workspaces WHERE archived_at IS NULL").all();
|
|
1220
|
+
return rows.map((r) => ({
|
|
1221
|
+
workspaceId: r.id,
|
|
1222
|
+
repoPath: r.repo_path,
|
|
1223
|
+
name: r.name,
|
|
1224
|
+
lastUpdated: 0
|
|
1225
|
+
}));
|
|
1226
|
+
}
|
|
1227
|
+
async function loadWorkspaceState(workspaceId2, repoPath) {
|
|
1228
|
+
const db = getDb();
|
|
1229
|
+
const wsRow = db.prepare("SELECT board_revision FROM workspaces WHERE id = ?").get(workspaceId2);
|
|
1230
|
+
const revision = wsRow?.board_revision ?? 0;
|
|
1231
|
+
const board = loadBoardInternal(workspaceId2);
|
|
1232
|
+
const projectConfig = loadProjectConfigInternal(workspaceId2);
|
|
1233
|
+
return {
|
|
1234
|
+
workspaceId: workspaceId2,
|
|
1235
|
+
repoPath,
|
|
1236
|
+
board,
|
|
1237
|
+
revision,
|
|
1238
|
+
projectConfig
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
async function saveWorkspaceState(workspaceId2, request) {
|
|
1242
|
+
const db = getDb();
|
|
1243
|
+
const tx = db.transaction(() => {
|
|
1244
|
+
const wsRow = db.prepare("SELECT board_revision FROM workspaces WHERE id = ?").get(workspaceId2);
|
|
1245
|
+
const currentRevision = wsRow?.board_revision ?? 0;
|
|
1246
|
+
if (request.revision !== currentRevision) {
|
|
1247
|
+
throw new Error(`Revision conflict: expected ${currentRevision}, got ${request.revision}`);
|
|
1248
|
+
}
|
|
1249
|
+
saveBoardInternal(workspaceId2, request.board);
|
|
1250
|
+
const newRevision = currentRevision + 1;
|
|
1251
|
+
db.prepare("UPDATE workspaces SET board_revision = ?, updated_at = ? WHERE id = ?").run(
|
|
1252
|
+
newRevision,
|
|
1253
|
+
Date.now(),
|
|
1254
|
+
workspaceId2
|
|
1255
|
+
);
|
|
1256
|
+
return { revision: newRevision };
|
|
1257
|
+
});
|
|
1258
|
+
return tx();
|
|
1259
|
+
}
|
|
1260
|
+
async function clearCardSession(workspaceId2, cardId) {
|
|
1261
|
+
const db = getDb();
|
|
1262
|
+
const tx = db.transaction(() => {
|
|
1263
|
+
const result = db.prepare("UPDATE cards SET worktree_path = NULL, updated_at = ? WHERE id = ? AND workspace_id = ?").run(Date.now(), cardId, workspaceId2);
|
|
1264
|
+
if (result.changes > 0) bumpBoardRevision(db, workspaceId2);
|
|
1265
|
+
});
|
|
1266
|
+
tx();
|
|
1267
|
+
}
|
|
1268
|
+
async function moveCard(workspaceId2, cardId, targetColumnId, targetIndex) {
|
|
1269
|
+
const db = getDb();
|
|
1270
|
+
const tx = db.transaction(() => {
|
|
1271
|
+
const cardRow = db.prepare("SELECT column_id FROM cards WHERE id = ? AND workspace_id = ?").get(cardId, workspaceId2);
|
|
1272
|
+
if (!cardRow) return;
|
|
1273
|
+
const sourceColumnId = cardRow.column_id;
|
|
1274
|
+
const sameColumn = sourceColumnId === targetColumnId;
|
|
1275
|
+
const sourceCards = db.prepare("SELECT id FROM cards WHERE workspace_id = ? AND column_id = ? AND id != ? ORDER BY column_position").all(workspaceId2, sourceColumnId, cardId);
|
|
1276
|
+
const targetCards = sameColumn ? sourceCards : db.prepare("SELECT id FROM cards WHERE workspace_id = ? AND column_id = ? ORDER BY column_position").all(workspaceId2, targetColumnId);
|
|
1277
|
+
const insertAt = typeof targetIndex === "number" ? targetIndex : targetCards.length;
|
|
1278
|
+
const finalTarget = [...targetCards];
|
|
1279
|
+
finalTarget.splice(insertAt, 0, { id: cardId });
|
|
1280
|
+
const now = Date.now();
|
|
1281
|
+
const updateTarget = db.prepare("UPDATE cards SET column_id = ?, column_position = ?, updated_at = ? WHERE id = ?");
|
|
1282
|
+
for (let i = 0; i < finalTarget.length; i++) {
|
|
1283
|
+
updateTarget.run(targetColumnId, i, now, finalTarget[i].id);
|
|
1284
|
+
}
|
|
1285
|
+
if (!sameColumn) {
|
|
1286
|
+
const updateSource = db.prepare("UPDATE cards SET column_position = ?, updated_at = ? WHERE id = ?");
|
|
1287
|
+
for (let i = 0; i < sourceCards.length; i++) {
|
|
1288
|
+
updateSource.run(i, now, sourceCards[i].id);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
bumpBoardRevision(db, workspaceId2);
|
|
1292
|
+
});
|
|
1293
|
+
tx();
|
|
1294
|
+
return loadBoardInternal(workspaceId2);
|
|
1295
|
+
}
|
|
1296
|
+
async function createCard(workspaceId2, data, baseRef) {
|
|
1297
|
+
const db = getDb();
|
|
1298
|
+
const id = generateTaskId();
|
|
1299
|
+
const now = Date.now();
|
|
1300
|
+
const type = data.type ?? "task";
|
|
1301
|
+
const columnId = data.columnId ?? "todo";
|
|
1302
|
+
const projectConfig = loadProjectConfigInternal(workspaceId2);
|
|
1303
|
+
const workflow = resolveWorkflowForCard(projectConfig.workflows, { workflowId: data.workflowId, type });
|
|
1304
|
+
const modelConfig = data.modelConfig ?? snapshotModelConfig(workflow);
|
|
1305
|
+
const card = {
|
|
1306
|
+
id,
|
|
1307
|
+
description: data.description,
|
|
1308
|
+
columnId,
|
|
1309
|
+
type,
|
|
1310
|
+
readyForDev: data.readyForDev ?? type === "story",
|
|
1311
|
+
agentId: data.agentId,
|
|
1312
|
+
priority: data.priority,
|
|
1313
|
+
dependsOn: data.dependsOn,
|
|
1314
|
+
waitsFor: data.waitsFor ?? [],
|
|
1315
|
+
subtaskIds: data.subtaskIds ?? [],
|
|
1316
|
+
autoFixAttempts: 0,
|
|
1317
|
+
activeLevel: data.activeLevel ?? highestWorkflowLevel(workflow),
|
|
1318
|
+
modelConfig,
|
|
1319
|
+
baseRef,
|
|
1320
|
+
createdAt: now,
|
|
1321
|
+
updatedAt: now,
|
|
1322
|
+
githubIssueUrl: data.githubIssueUrl,
|
|
1323
|
+
// Persist the resolved workflow id (not the raw input) so a card always records
|
|
1324
|
+
// which workflow it runs — otherwise an omitted workflowId leaves the card unlinked.
|
|
1325
|
+
workflowId: data.workflowId ?? workflow?.id,
|
|
1326
|
+
descriptionAttachments: data.descriptionAttachments ?? [],
|
|
1327
|
+
branchName: data.branchName,
|
|
1328
|
+
reviewComments: [],
|
|
1329
|
+
activityLog: [],
|
|
1330
|
+
terminalSessions: [],
|
|
1331
|
+
githubCommentIds: []
|
|
1332
|
+
};
|
|
1333
|
+
const tx = db.transaction(() => {
|
|
1334
|
+
if (card.dependsOn && !db.prepare("SELECT 1 FROM cards WHERE id = ?").get(card.dependsOn)) {
|
|
1335
|
+
card.dependsOn = void 0;
|
|
1336
|
+
}
|
|
1337
|
+
const countRow = db.prepare("SELECT COUNT(*) AS n FROM cards WHERE workspace_id = ? AND column_id = ?").get(workspaceId2, columnId);
|
|
1338
|
+
upsertCardRow(db, workspaceId2, card, countRow.n);
|
|
1339
|
+
replaceCardWaitsFor(db, card.id, card.waitsFor ?? []);
|
|
1340
|
+
replaceCardSubtasks(db, card.id, card.subtaskIds ?? []);
|
|
1341
|
+
bumpBoardRevision(db, workspaceId2);
|
|
1342
|
+
});
|
|
1343
|
+
tx();
|
|
1344
|
+
return card;
|
|
1345
|
+
}
|
|
1346
|
+
async function appendActivityLog(workspaceId2, cardId, message) {
|
|
1347
|
+
const db = getDb();
|
|
1348
|
+
const tx = db.transaction(() => {
|
|
1349
|
+
const exists = db.prepare("SELECT 1 FROM cards WHERE id = ? AND workspace_id = ?").get(cardId, workspaceId2);
|
|
1350
|
+
if (!exists) return;
|
|
1351
|
+
const now = Date.now();
|
|
1352
|
+
db.prepare("INSERT INTO activity_log (card_id, timestamp, message) VALUES (?, ?, ?)").run(cardId, now, message);
|
|
1353
|
+
db.prepare("UPDATE cards SET updated_at = ? WHERE id = ?").run(now, cardId);
|
|
1354
|
+
});
|
|
1355
|
+
tx();
|
|
1356
|
+
}
|
|
1357
|
+
async function saveTerminalBuffer(workspaceId2, streamId, data) {
|
|
1358
|
+
const filePath = bufferFilePath(workspaceId2, streamId);
|
|
1359
|
+
await mkdir(join6(workspaceDirPath(workspaceId2), "buffers"), { recursive: true });
|
|
1360
|
+
await writeFile(filePath, data, "utf-8");
|
|
1361
|
+
}
|
|
1362
|
+
async function loadTerminalBuffer(workspaceId2, streamId) {
|
|
1363
|
+
try {
|
|
1364
|
+
return await readFile(bufferFilePath(workspaceId2, streamId), "utf-8");
|
|
1365
|
+
} catch {
|
|
1366
|
+
return "";
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
async function appendTerminalSession(workspaceId2, cardId, entry) {
|
|
1370
|
+
const db = getDb();
|
|
1371
|
+
const tx = db.transaction(() => {
|
|
1372
|
+
const exists = db.prepare("SELECT 1 FROM cards WHERE id = ? AND workspace_id = ?").get(cardId, workspaceId2);
|
|
1373
|
+
if (!exists) return;
|
|
1374
|
+
db.prepare(
|
|
1375
|
+
`INSERT INTO terminal_sessions (card_id, stream_id, type, started_at, ended_at, agent_id, state)
|
|
1376
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1377
|
+
ON CONFLICT(card_id, stream_id) DO UPDATE SET
|
|
1378
|
+
type = excluded.type,
|
|
1379
|
+
started_at = excluded.started_at,
|
|
1380
|
+
ended_at = excluded.ended_at,
|
|
1381
|
+
agent_id = excluded.agent_id,
|
|
1382
|
+
state = excluded.state`
|
|
1383
|
+
).run(
|
|
1384
|
+
cardId,
|
|
1385
|
+
entry.streamId,
|
|
1386
|
+
entry.type,
|
|
1387
|
+
entry.startedAt,
|
|
1388
|
+
entry.endedAt ?? null,
|
|
1389
|
+
entry.agentId ?? null,
|
|
1390
|
+
entry.state ?? null
|
|
1391
|
+
);
|
|
1392
|
+
db.prepare("UPDATE cards SET updated_at = ? WHERE id = ?").run(Date.now(), cardId);
|
|
1393
|
+
});
|
|
1394
|
+
tx();
|
|
1395
|
+
}
|
|
1396
|
+
async function closeAllOpenTerminalSessions(workspaceId2, cardId, endedAt) {
|
|
1397
|
+
const db = getDb();
|
|
1398
|
+
const tx = db.transaction(() => {
|
|
1399
|
+
const result = db.prepare("UPDATE terminal_sessions SET ended_at = ?, state = 'killed' WHERE card_id = ? AND ended_at IS NULL").run(endedAt, cardId);
|
|
1400
|
+
if (result.changes > 0) bumpBoardRevision(db, workspaceId2);
|
|
1401
|
+
});
|
|
1402
|
+
tx();
|
|
1403
|
+
}
|
|
1404
|
+
async function endTerminalSession(workspaceId2, cardId, streamId, endedAt, state) {
|
|
1405
|
+
const db = getDb();
|
|
1406
|
+
const tx = db.transaction(() => {
|
|
1407
|
+
const result = state ? db.prepare("UPDATE terminal_sessions SET ended_at = ?, state = ? WHERE card_id = ? AND stream_id = ?").run(endedAt, state, cardId, streamId) : db.prepare("UPDATE terminal_sessions SET ended_at = ? WHERE card_id = ? AND stream_id = ?").run(endedAt, cardId, streamId);
|
|
1408
|
+
if (result.changes > 0) bumpBoardRevision(db, workspaceId2);
|
|
1409
|
+
});
|
|
1410
|
+
tx();
|
|
1411
|
+
}
|
|
1412
|
+
async function linkCommentToSession(workspaceId2, cardId, commentCreatedAt, streamId) {
|
|
1413
|
+
const db = getDb();
|
|
1414
|
+
const tx = db.transaction(() => {
|
|
1415
|
+
const result = db.prepare("UPDATE review_comments SET stream_id = ? WHERE card_id = ? AND created_at = ?").run(streamId, cardId, commentCreatedAt);
|
|
1416
|
+
if (result.changes > 0) bumpBoardRevision(db, workspaceId2);
|
|
1417
|
+
});
|
|
1418
|
+
tx();
|
|
1419
|
+
}
|
|
1420
|
+
async function stampReviewCommentMetadata(workspaceId2, cardId, commentCreatedAt, patch) {
|
|
1421
|
+
const db = getDb();
|
|
1422
|
+
const tx = db.transaction(() => {
|
|
1423
|
+
const row = db.prepare("SELECT metadata_json FROM review_comments WHERE card_id = ? AND created_at = ?").get(cardId, commentCreatedAt);
|
|
1424
|
+
if (!row) return;
|
|
1425
|
+
const current = safeJsonParse(row.metadata_json, {});
|
|
1426
|
+
const result = db.prepare("UPDATE review_comments SET metadata_json = ? WHERE card_id = ? AND created_at = ?").run(JSON.stringify({ ...current, ...patch }), cardId, commentCreatedAt);
|
|
1427
|
+
if (result.changes > 0) bumpBoardRevision(db, workspaceId2);
|
|
1428
|
+
});
|
|
1429
|
+
tx();
|
|
1430
|
+
}
|
|
1431
|
+
function extractSignedImageUrls(bodyHtml) {
|
|
1432
|
+
const map = /* @__PURE__ */ new Map();
|
|
1433
|
+
const cdnPattern = /https:\/\/private-user-images\.githubusercontent\.com\/[^"'<>\s]+/g;
|
|
1434
|
+
const uuidPattern = /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})\./i;
|
|
1435
|
+
for (const cdnUrl of bodyHtml.match(cdnPattern) ?? []) {
|
|
1436
|
+
const uuidMatch = cdnUrl.match(uuidPattern);
|
|
1437
|
+
if (uuidMatch?.[1]) map.set(uuidMatch[1].toLowerCase(), cdnUrl);
|
|
1438
|
+
}
|
|
1439
|
+
return map;
|
|
1440
|
+
}
|
|
1441
|
+
async function downloadGithubImages(text, cardId, _workspaceId, fetchHtml) {
|
|
1442
|
+
const assetPattern = /https:\/\/github\.com\/user-attachments\/assets\/[^\s"'<>\]]+/g;
|
|
1443
|
+
const assetUrls = [...new Set(text.match(assetPattern) ?? [])];
|
|
1444
|
+
if (assetUrls.length === 0) return text;
|
|
1445
|
+
let signedUrlMap;
|
|
1446
|
+
const tryDownload = async (url) => {
|
|
1447
|
+
const res = await fetch(url);
|
|
1448
|
+
if (!res.ok) return null;
|
|
1449
|
+
return res;
|
|
1450
|
+
};
|
|
1451
|
+
let result = text;
|
|
1452
|
+
for (const assetUrl of assetUrls) {
|
|
1453
|
+
try {
|
|
1454
|
+
let res = await tryDownload(assetUrl);
|
|
1455
|
+
if (!res && fetchHtml) {
|
|
1456
|
+
if (!signedUrlMap) {
|
|
1457
|
+
const html = await fetchHtml();
|
|
1458
|
+
signedUrlMap = html ? extractSignedImageUrls(html) : /* @__PURE__ */ new Map();
|
|
1459
|
+
}
|
|
1460
|
+
const assetUuid = assetUrl.split("/").pop()?.toLowerCase() ?? "";
|
|
1461
|
+
const signedUrl = signedUrlMap.get(assetUuid);
|
|
1462
|
+
if (signedUrl) res = await tryDownload(signedUrl);
|
|
1463
|
+
}
|
|
1464
|
+
if (!res) continue;
|
|
1465
|
+
const contentType = res.headers.get("content-type") ?? "image/png";
|
|
1466
|
+
const extMap = {
|
|
1467
|
+
"image/png": "png",
|
|
1468
|
+
"image/jpeg": "jpg",
|
|
1469
|
+
"image/gif": "gif",
|
|
1470
|
+
"image/webp": "webp"
|
|
1471
|
+
};
|
|
1472
|
+
const ext = extMap[contentType.split(";")[0]?.trim() ?? ""] ?? "png";
|
|
1473
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
1474
|
+
const localPath = await saveAttachment(buffer, ext, cardId);
|
|
1475
|
+
const parts = localPath.replace(/\\/g, "/").split("/");
|
|
1476
|
+
const filename = parts[parts.length - 1];
|
|
1477
|
+
const localUrl = `/api/attachments/${encodeURIComponent(cardId)}/${encodeURIComponent(filename)}`;
|
|
1478
|
+
result = result.replaceAll(assetUrl, localUrl);
|
|
1479
|
+
} catch {
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return result;
|
|
1483
|
+
}
|
|
1484
|
+
async function saveAttachment(data, ext, cardId) {
|
|
1485
|
+
const dir = join6(ATTACHMENTS_DIR, cardId);
|
|
1486
|
+
await mkdir(dir, { recursive: true });
|
|
1487
|
+
const hash = createHash("sha256").update(data).digest("hex");
|
|
1488
|
+
const filePath = join6(dir, `${hash}.${ext}`);
|
|
1489
|
+
if (!existsSync2(filePath)) {
|
|
1490
|
+
await writeFile(filePath, data);
|
|
1491
|
+
}
|
|
1492
|
+
return filePath;
|
|
1493
|
+
}
|
|
1494
|
+
async function updateCard(workspaceId2, cardId, update) {
|
|
1495
|
+
const db = getDb();
|
|
1496
|
+
const tx = db.transaction(() => {
|
|
1497
|
+
const row = db.prepare("SELECT * FROM cards WHERE id = ? AND workspace_id = ?").get(cardId, workspaceId2);
|
|
1498
|
+
if (!row) return null;
|
|
1499
|
+
const existing = cardFromRow(row, loadCardChildren(db, cardId));
|
|
1500
|
+
const updated = { ...existing, ...update, updatedAt: Date.now() };
|
|
1501
|
+
if (updated.dependsOn && !db.prepare("SELECT 1 FROM cards WHERE id = ?").get(updated.dependsOn)) {
|
|
1502
|
+
updated.dependsOn = void 0;
|
|
1503
|
+
}
|
|
1504
|
+
upsertCardRow(db, workspaceId2, updated, row.column_position);
|
|
1505
|
+
if (update.waitsFor !== void 0) replaceCardWaitsFor(db, cardId, update.waitsFor);
|
|
1506
|
+
if (update.subtaskIds !== void 0) replaceCardSubtasks(db, cardId, update.subtaskIds);
|
|
1507
|
+
if (update.reviewComments !== void 0) {
|
|
1508
|
+
db.prepare("DELETE FROM review_comments WHERE card_id = ?").run(cardId);
|
|
1509
|
+
const insertReview = db.prepare(
|
|
1510
|
+
`INSERT INTO review_comments (
|
|
1511
|
+
comment_id, card_id, created_at, type, actor_type, actor_id, actor_source,
|
|
1512
|
+
status, stream_id, summary, issues_json, attachments_json, metadata_json
|
|
1513
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1514
|
+
);
|
|
1515
|
+
for (const c of update.reviewComments ?? []) {
|
|
1516
|
+
insertReview.run(
|
|
1517
|
+
c.id,
|
|
1518
|
+
cardId,
|
|
1519
|
+
c.createdAt,
|
|
1520
|
+
c.type,
|
|
1521
|
+
c.actor.type,
|
|
1522
|
+
c.actor.id,
|
|
1523
|
+
c.actor.source ?? null,
|
|
1524
|
+
c.status ?? null,
|
|
1525
|
+
c.streamId ?? null,
|
|
1526
|
+
c.summary,
|
|
1527
|
+
JSON.stringify(c.issues ?? []),
|
|
1528
|
+
JSON.stringify(c.attachments ?? []),
|
|
1529
|
+
JSON.stringify(c.metadata ?? {})
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
bumpBoardRevision(db, workspaceId2);
|
|
1534
|
+
return updated;
|
|
1535
|
+
});
|
|
1536
|
+
const result = tx();
|
|
1537
|
+
return result;
|
|
1538
|
+
}
|
|
1539
|
+
async function deleteCard(workspaceId2, cardId) {
|
|
1540
|
+
const db = getDb();
|
|
1541
|
+
const tx = db.transaction(() => {
|
|
1542
|
+
const cardRow = db.prepare("SELECT column_id FROM cards WHERE id = ? AND workspace_id = ?").get(cardId, workspaceId2);
|
|
1543
|
+
if (!cardRow) return;
|
|
1544
|
+
db.prepare("DELETE FROM memories WHERE origin_card_id = ? AND status = 'pending'").run(cardId);
|
|
1545
|
+
db.prepare("DELETE FROM cards WHERE id = ?").run(cardId);
|
|
1546
|
+
const remaining = db.prepare("SELECT id FROM cards WHERE workspace_id = ? AND column_id = ? ORDER BY column_position").all(workspaceId2, cardRow.column_id);
|
|
1547
|
+
const updatePos = db.prepare("UPDATE cards SET column_position = ?, updated_at = ? WHERE id = ?");
|
|
1548
|
+
const now = Date.now();
|
|
1549
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
1550
|
+
updatePos.run(i, now, remaining[i].id);
|
|
1551
|
+
}
|
|
1552
|
+
bumpBoardRevision(db, workspaceId2);
|
|
1553
|
+
});
|
|
1554
|
+
tx();
|
|
1555
|
+
try {
|
|
1556
|
+
const { rm } = await import("node:fs/promises");
|
|
1557
|
+
await rm(join6(ATTACHMENTS_DIR, cardId), { recursive: true, force: true });
|
|
1558
|
+
} catch {
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
async function removeWorkspace(workspaceId2) {
|
|
1562
|
+
const db = getDb();
|
|
1563
|
+
db.prepare("DELETE FROM workspaces WHERE id = ?").run(workspaceId2);
|
|
1564
|
+
}
|
|
1565
|
+
var INSERT_CARD_WAITS_FOR_IF_EXISTS, INSERT_CARD_SUBTASK_IF_EXISTS;
|
|
1566
|
+
var init_workspace_state = __esm({
|
|
1567
|
+
"src/state/workspace-state.ts"() {
|
|
1568
|
+
"use strict";
|
|
1569
|
+
init_runtime_config();
|
|
1570
|
+
init_api_contract();
|
|
1571
|
+
init_task_id();
|
|
1572
|
+
init_db();
|
|
1573
|
+
init_secrets_crypto();
|
|
1574
|
+
INSERT_CARD_WAITS_FOR_IF_EXISTS = `
|
|
1575
|
+
INSERT INTO card_waits_for (card_id, waits_for_id)
|
|
1576
|
+
SELECT ?, ? WHERE EXISTS (SELECT 1 FROM cards WHERE id = ?) AND ? != ?
|
|
1577
|
+
`;
|
|
1578
|
+
INSERT_CARD_SUBTASK_IF_EXISTS = `
|
|
1579
|
+
INSERT INTO card_subtasks (story_id, subtask_id)
|
|
1580
|
+
SELECT ?, ? WHERE EXISTS (SELECT 1 FROM cards WHERE id = ?) AND ? != ?
|
|
1581
|
+
`;
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
// src/mcp/kanban-mcp-server.ts
|
|
1586
|
+
init_api_contract();
|
|
1587
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1588
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1589
|
+
import { z as z2 } from "zod";
|
|
1590
|
+
var serverUrl = process.argv[2] ?? process.env.WHIPPED_SERVER_URL ?? "http://127.0.0.1:3000";
|
|
1591
|
+
var workspaceId = process.argv[3] ?? process.env.WHIPPED_WORKSPACE_ID ?? "";
|
|
1592
|
+
var agentId = process.argv[4] && !process.argv[4].startsWith("--") ? process.argv[4] : "claude";
|
|
1593
|
+
function namedArg(flag) {
|
|
1594
|
+
const hit = process.argv.find((a) => a.startsWith(`${flag}=`));
|
|
1595
|
+
return hit ? hit.slice(flag.length + 1) : void 0;
|
|
1596
|
+
}
|
|
1597
|
+
var mcpRole = namedArg("--role") ?? "task";
|
|
1598
|
+
var recurringAgentId = namedArg("--recurring-agent-id") ?? "";
|
|
1599
|
+
var apiToken = process.env.WHIPPED_API_TOKEN ?? "";
|
|
1600
|
+
var authHeaders = apiToken ? { "x-whipped-token": apiToken } : {};
|
|
1601
|
+
var ROUTES = {
|
|
1602
|
+
"workspace.state": { method: "GET", path: () => "workspace/state" },
|
|
1603
|
+
"workflows.list": { method: "GET", path: () => "workflows" },
|
|
1604
|
+
"projectConfig.get": { method: "GET", path: () => "project-config" },
|
|
1605
|
+
"memory.search": { method: "GET", path: () => "memory/search" },
|
|
1606
|
+
"memory.get": { method: "GET", path: (i) => `memory/${i.id}` },
|
|
1607
|
+
"cards.create": { method: "POST", path: () => "cards" },
|
|
1608
|
+
"cards.update": { method: "PATCH", path: (i) => `cards/${i.cardId}` },
|
|
1609
|
+
"cards.move": { method: "POST", path: () => "cards/move" },
|
|
1610
|
+
"cards.delete": { method: "DELETE", path: (i) => `cards/${i.cardId}` },
|
|
1611
|
+
"cards.addReviewComment": { method: "POST", path: () => "cards/add-review-comment" },
|
|
1612
|
+
"cards.interruptTask": { method: "POST", path: () => "cards/interrupt-task" },
|
|
1613
|
+
"cards.setPrMeta": { method: "POST", path: () => "cards/set-pr-meta" },
|
|
1614
|
+
"cards.setPlan": { method: "POST", path: () => "cards/set-plan" },
|
|
1615
|
+
"workflows.upsert": { method: "POST", path: () => "workflows" },
|
|
1616
|
+
"projectConfig.setGitInstructions": { method: "POST", path: () => "project-config/git-instructions" },
|
|
1617
|
+
"projectConfig.setSystemPrompt": { method: "POST", path: () => "project-config/system-prompt" },
|
|
1618
|
+
"memory.propose": { method: "POST", path: () => "memory/propose" },
|
|
1619
|
+
"memory.proposeUpdate": { method: "POST", path: () => "memory/propose-update" },
|
|
1620
|
+
"recurring.list": { method: "GET", path: () => "recurring-agents" },
|
|
1621
|
+
"recurring.create": { method: "POST", path: () => "recurring-agents" },
|
|
1622
|
+
"recurring.update": { method: "PATCH", path: (i) => `recurring-agents/${i.id}` },
|
|
1623
|
+
"recurring.delete": { method: "DELETE", path: (i) => `recurring-agents/${i.id}` },
|
|
1624
|
+
"recurring.setJournal": { method: "POST", path: (i) => `recurring-agents/${i.id}/journal` }
|
|
1625
|
+
};
|
|
1626
|
+
async function apiMutate(procedure, input) {
|
|
1627
|
+
const route = ROUTES[procedure];
|
|
1628
|
+
if (!route) throw new Error(`Unknown procedure: ${procedure}`);
|
|
1629
|
+
const res = await fetch(`${serverUrl}/api/${route.path(input)}`, {
|
|
1630
|
+
method: route.method,
|
|
1631
|
+
headers: { "Content-Type": "application/json", ...authHeaders },
|
|
1632
|
+
body: JSON.stringify(input),
|
|
1633
|
+
signal: AbortSignal.timeout(15e3)
|
|
1634
|
+
});
|
|
1635
|
+
if (!res.ok) throw new Error(`${procedure} failed: ${res.status} ${await res.text()}`);
|
|
1636
|
+
return await res.json();
|
|
1637
|
+
}
|
|
1638
|
+
async function apiQuery(procedure, input) {
|
|
1639
|
+
const route = ROUTES[procedure];
|
|
1640
|
+
if (!route) throw new Error(`Unknown procedure: ${procedure}`);
|
|
1641
|
+
const qs = new URLSearchParams();
|
|
1642
|
+
for (const [k, v] of Object.entries(input)) {
|
|
1643
|
+
if (v !== void 0 && v !== null) qs.set(k, String(v));
|
|
1644
|
+
}
|
|
1645
|
+
const query = qs.toString();
|
|
1646
|
+
const res = await fetch(`${serverUrl}/api/${route.path(input)}${query ? `?${query}` : ""}`, {
|
|
1647
|
+
headers: authHeaders,
|
|
1648
|
+
signal: AbortSignal.timeout(15e3)
|
|
1649
|
+
});
|
|
1650
|
+
if (!res.ok) throw new Error(`${procedure} failed: ${res.status} ${await res.text()}`);
|
|
1651
|
+
return await res.json();
|
|
1652
|
+
}
|
|
1653
|
+
var server = new McpServer({ name: "whipped", version: "1.0.0" });
|
|
1654
|
+
var RECURRING_OBSERVER_TOOLS = /* @__PURE__ */ new Set([
|
|
1655
|
+
"kanban_get_board",
|
|
1656
|
+
"kanban_create_card",
|
|
1657
|
+
"kanban_add_comment",
|
|
1658
|
+
"kanban_get_workflows",
|
|
1659
|
+
"kanban_get_pr_meta",
|
|
1660
|
+
"kanban_get_git_instructions",
|
|
1661
|
+
"kanban_get_system_prompt",
|
|
1662
|
+
"whipped_search_memory",
|
|
1663
|
+
"whipped_get_memory",
|
|
1664
|
+
"update_journal"
|
|
1665
|
+
]);
|
|
1666
|
+
var baseRegisterTool = server.registerTool;
|
|
1667
|
+
var registerTool = ((name, ...rest) => {
|
|
1668
|
+
if (mcpRole === "recurring" && !RECURRING_OBSERVER_TOOLS.has(name)) return void 0;
|
|
1669
|
+
return baseRegisterTool.call(server, name, ...rest);
|
|
1670
|
+
});
|
|
1671
|
+
var attachmentInputSchema = z2.object({
|
|
1672
|
+
type: z2.string().describe("Attachment type \u2014 'image' for images, 'file' for other files"),
|
|
1673
|
+
name: z2.string().describe("Human-readable name for the attachment"),
|
|
1674
|
+
mimeType: z2.string().describe("MIME type, e.g. image/png, application/pdf"),
|
|
1675
|
+
path: z2.string().describe("Absolute file path to read from")
|
|
1676
|
+
});
|
|
1677
|
+
async function processAttachments(attachments, cardId) {
|
|
1678
|
+
const { readFile: readFile2 } = await import("node:fs/promises");
|
|
1679
|
+
const { saveAttachment: saveAttachment2 } = await Promise.resolve().then(() => (init_workspace_state(), workspace_state_exports));
|
|
1680
|
+
const results = [];
|
|
1681
|
+
for (const att of attachments) {
|
|
1682
|
+
try {
|
|
1683
|
+
const data = await readFile2(att.path);
|
|
1684
|
+
const ext = att.path.split(".").pop() ?? "bin";
|
|
1685
|
+
const canonicalPath = await saveAttachment2(data, ext, cardId);
|
|
1686
|
+
results.push({ type: att.type, name: att.name, mimeType: att.mimeType, path: canonicalPath });
|
|
1687
|
+
} catch {
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
return results;
|
|
1691
|
+
}
|
|
1692
|
+
registerTool(
|
|
1693
|
+
"kanban_get_board",
|
|
1694
|
+
{
|
|
1695
|
+
description: "Get the current Kanban board state including all cards and their columns.",
|
|
1696
|
+
inputSchema: {}
|
|
1697
|
+
},
|
|
1698
|
+
async () => {
|
|
1699
|
+
const state = await apiQuery("workspace.state", {
|
|
1700
|
+
workspaceId
|
|
1701
|
+
});
|
|
1702
|
+
const board = state.board;
|
|
1703
|
+
const lines = [];
|
|
1704
|
+
for (const col of board.columns) {
|
|
1705
|
+
if (col.taskIds.length === 0) continue;
|
|
1706
|
+
lines.push(`## ${col.title}`);
|
|
1707
|
+
for (const id of col.taskIds) {
|
|
1708
|
+
const card = board.cards[id];
|
|
1709
|
+
if (!card) continue;
|
|
1710
|
+
const typeTag = card.type && card.type !== "task" ? ` [${card.type}]` : "";
|
|
1711
|
+
const priorityTag = card.priority ? ` [${card.priority}]` : "";
|
|
1712
|
+
const waitsTag = card.waitsFor && card.waitsFor.length > 0 ? ` (waits for: ${card.waitsFor.join(", ")})` : "";
|
|
1713
|
+
const depsTag = card.dependsOn ? ` (depends on: ${card.dependsOn})` : waitsTag;
|
|
1714
|
+
const cardDisplay = (card.description?.split("\n")[0] ?? "").slice(0, 80) || id;
|
|
1715
|
+
lines.push(`- [${id}]${typeTag} ${cardDisplay}${priorityTag}${depsTag}`);
|
|
1716
|
+
if (card.type === "story" && card.subtaskIds && card.subtaskIds.length > 0) {
|
|
1717
|
+
const met = card.subtaskIds.filter((subId) => {
|
|
1718
|
+
const sub = board.cards[subId];
|
|
1719
|
+
return sub?.columnId === "ready_for_review" || sub?.columnId === "done";
|
|
1720
|
+
});
|
|
1721
|
+
lines.push(` Progress: ${met.length}/${card.subtaskIds.length} subtasks complete`);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
return { content: [{ type: "text", text: lines.join("\n") || "Board is empty." }] };
|
|
1726
|
+
}
|
|
1727
|
+
);
|
|
1728
|
+
registerTool(
|
|
1729
|
+
"kanban_create_story",
|
|
1730
|
+
{
|
|
1731
|
+
description: `Create a story ticket with its subtasks in one atomic operation.
|
|
1732
|
+
|
|
1733
|
+
Creates all subtasks first, then the story card that depends on them. The story triggers its orchestrator workflow automatically when all subtasks reach 'Ready for Review' or 'Done'.
|
|
1734
|
+
|
|
1735
|
+
**Intra-batch dependencies:** If subtask B should run after subtask A (both in this batch), give subtask A a \`tempId\` (e.g. "auth") and list that tempId in subtask B's \`dependsOn\`. The real card IDs are wired up automatically after creation.`,
|
|
1736
|
+
inputSchema: {
|
|
1737
|
+
description: z2.string().describe(
|
|
1738
|
+
"Story description \u2014 what this story accomplishes as a whole, including acceptance criteria. The first line serves as the story title."
|
|
1739
|
+
),
|
|
1740
|
+
priority: z2.enum(["urgent", "high", "medium", "low"]).optional().describe("Story priority"),
|
|
1741
|
+
workflowId: z2.string().min(1).describe(
|
|
1742
|
+
"REQUIRED. ID of the story orchestrator workflow. Call kanban_get_workflows first and pick the story workflow that best matches this story's scope; fall back to the default story workflow only if none fits."
|
|
1743
|
+
),
|
|
1744
|
+
baseRef: z2.string().optional().describe("Base branch for all cards in this story. Omit to use the repo default branch."),
|
|
1745
|
+
activeLevel: z2.enum(["minimal", "low", "medium", "high", "max"]).optional().describe(
|
|
1746
|
+
"Capability level for the story's orchestrator (orch) workflow. Each subtask sets its own level. Omit to default to the workflow's highest configured tier."
|
|
1747
|
+
),
|
|
1748
|
+
attachments: z2.array(attachmentInputSchema).optional().describe(
|
|
1749
|
+
"Files to attach (e.g. design docs, screenshots). Reference each one inline in the story description as [Attachment #N], where N is its 1-based position in this array."
|
|
1750
|
+
),
|
|
1751
|
+
subtasks: z2.array(
|
|
1752
|
+
z2.object({
|
|
1753
|
+
tempId: z2.string().optional().describe(
|
|
1754
|
+
"Short label to reference this subtask from other subtasks' dependsOn in this batch (e.g. 'auth', 'db-schema'). Not stored \u2014 only used for wiring up intra-batch deps."
|
|
1755
|
+
),
|
|
1756
|
+
description: z2.string().describe("Subtask description with acceptance criteria. The first line serves as the subtask title."),
|
|
1757
|
+
priority: z2.enum(["urgent", "high", "medium", "low"]).optional().describe("Subtask priority"),
|
|
1758
|
+
workflowId: z2.string().min(1).describe(
|
|
1759
|
+
"REQUIRED. Workflow ID for this subtask (from kanban_get_workflows). Pick the workflow matching the subtask's nature (e.g. frontend vs backend); fall back to the default task workflow only if none fits."
|
|
1760
|
+
),
|
|
1761
|
+
baseRef: z2.string().optional().describe("Override base branch for this subtask only"),
|
|
1762
|
+
branchName: z2.string().min(1).describe(
|
|
1763
|
+
"REQUIRED. Git branch name for this subtask: '<type>/<slug>' (e.g. 'fix/user-auth-bug', 'feat/dark-mode'). All lowercase, dashes not underscores, \u226460 chars."
|
|
1764
|
+
),
|
|
1765
|
+
dependsOn: z2.array(z2.string()).optional().describe(
|
|
1766
|
+
"Dependencies for this subtask. Use real card IDs for existing cards on the board, or tempId values to reference other subtasks in this same batch."
|
|
1767
|
+
),
|
|
1768
|
+
activeLevel: z2.enum(["minimal", "low", "medium", "high", "max"]).optional().describe(
|
|
1769
|
+
"Capability level for this subtask's dev workflow. Omit to default to the workflow's highest configured tier; lower it for trivial subtasks."
|
|
1770
|
+
),
|
|
1771
|
+
attachments: z2.array(attachmentInputSchema).optional().describe(
|
|
1772
|
+
"Files to attach to this subtask. Reference each one inline in this subtask's description as [Attachment #N], where N is its 1-based position in this array."
|
|
1773
|
+
)
|
|
1774
|
+
})
|
|
1775
|
+
).min(1).describe(
|
|
1776
|
+
"Subtasks to create. At least one required. Each subtask gets type: 'subtask' and readyForDev: true automatically."
|
|
1777
|
+
)
|
|
1778
|
+
}
|
|
1779
|
+
},
|
|
1780
|
+
async ({ description, priority, workflowId, baseRef, activeLevel, attachments, subtasks }) => {
|
|
1781
|
+
const tempIdToRealId = /* @__PURE__ */ new Map();
|
|
1782
|
+
const createdSubtasks = [];
|
|
1783
|
+
for (const subtask of subtasks) {
|
|
1784
|
+
const existingDeps = (subtask.dependsOn ?? []).filter((dep) => !subtasks.some((s) => s.tempId === dep));
|
|
1785
|
+
const card = await apiMutate("cards.create", {
|
|
1786
|
+
workspaceId,
|
|
1787
|
+
description: subtask.description,
|
|
1788
|
+
type: "subtask",
|
|
1789
|
+
priority: subtask.priority,
|
|
1790
|
+
readyForDev: true,
|
|
1791
|
+
baseRef: subtask.baseRef || baseRef || void 0,
|
|
1792
|
+
workflowId: subtask.workflowId || void 0,
|
|
1793
|
+
branchName: subtask.branchName || void 0,
|
|
1794
|
+
activeLevel: subtask.activeLevel,
|
|
1795
|
+
dependsOn: existingDeps[0]
|
|
1796
|
+
});
|
|
1797
|
+
if (subtask.attachments?.length) {
|
|
1798
|
+
const processed = await processAttachments(subtask.attachments, card.id);
|
|
1799
|
+
if (processed.length) {
|
|
1800
|
+
await apiMutate("cards.update", {
|
|
1801
|
+
workspaceId,
|
|
1802
|
+
cardId: card.id,
|
|
1803
|
+
descriptionAttachments: processed,
|
|
1804
|
+
revision: 0
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
if (subtask.tempId) tempIdToRealId.set(subtask.tempId, card.id);
|
|
1809
|
+
createdSubtasks.push({
|
|
1810
|
+
realId: card.id,
|
|
1811
|
+
descFirst: subtask.description.split("\n")[0] ?? "",
|
|
1812
|
+
rawDeps: subtask.dependsOn ?? []
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
for (const { realId, rawDeps } of createdSubtasks) {
|
|
1816
|
+
const batchDeps = rawDeps.filter((dep) => tempIdToRealId.has(dep));
|
|
1817
|
+
if (batchDeps.length === 0) continue;
|
|
1818
|
+
const resolvedBatchDeps = batchDeps.map((dep) => tempIdToRealId.get(dep));
|
|
1819
|
+
const existingDeps = rawDeps.filter((dep) => !tempIdToRealId.has(dep));
|
|
1820
|
+
await apiMutate("cards.update", {
|
|
1821
|
+
workspaceId,
|
|
1822
|
+
cardId: realId,
|
|
1823
|
+
dependsOn: [...existingDeps, ...resolvedBatchDeps][0],
|
|
1824
|
+
revision: 0
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
const subtaskIds = createdSubtasks.map((s) => s.realId);
|
|
1828
|
+
const storyCard = await apiMutate("cards.create", {
|
|
1829
|
+
workspaceId,
|
|
1830
|
+
description,
|
|
1831
|
+
type: "story",
|
|
1832
|
+
priority,
|
|
1833
|
+
baseRef: baseRef || void 0,
|
|
1834
|
+
workflowId: workflowId || void 0,
|
|
1835
|
+
activeLevel,
|
|
1836
|
+
subtaskIds
|
|
1837
|
+
});
|
|
1838
|
+
if (attachments?.length) {
|
|
1839
|
+
const processed = await processAttachments(attachments, storyCard.id);
|
|
1840
|
+
if (processed.length) {
|
|
1841
|
+
await apiMutate("cards.update", {
|
|
1842
|
+
workspaceId,
|
|
1843
|
+
cardId: storyCard.id,
|
|
1844
|
+
descriptionAttachments: processed,
|
|
1845
|
+
revision: 0
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
const storyDisplay = description.split("\n")[0]?.slice(0, 80) ?? storyCard.id;
|
|
1850
|
+
const lines = [`Created story [${storyCard.id}] "${storyDisplay}" with ${subtaskIds.length} subtask(s):`];
|
|
1851
|
+
for (const { realId, descFirst } of createdSubtasks)
|
|
1852
|
+
lines.push(` Subtask [${realId}] "${descFirst.slice(0, 80)}"`);
|
|
1853
|
+
lines.push(`The story will trigger its orchestrator workflow once all subtasks complete.`);
|
|
1854
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1855
|
+
}
|
|
1856
|
+
);
|
|
1857
|
+
registerTool(
|
|
1858
|
+
"kanban_create_card",
|
|
1859
|
+
{
|
|
1860
|
+
description: "Create a new task card on the Kanban board.",
|
|
1861
|
+
inputSchema: {
|
|
1862
|
+
description: z2.string().describe("Full task description including acceptance criteria. The first line serves as the display title."),
|
|
1863
|
+
type: z2.enum(["task", "story", "subtask"]).optional().describe(
|
|
1864
|
+
"Card type \u2014 'task' (default), 'story' (orchestrator ticket with subtasks), or 'subtask' (child of a story)"
|
|
1865
|
+
),
|
|
1866
|
+
priority: z2.enum(["urgent", "high", "medium", "low"]).optional().describe("Task priority \u2014 urgent cards are dispatched first in autonomous mode"),
|
|
1867
|
+
readyForDev: z2.boolean().optional().describe(
|
|
1868
|
+
"Mark the card as ready for the agent to pick up automatically. Defaults to false (true for story cards)."
|
|
1869
|
+
),
|
|
1870
|
+
columnId: z2.enum(["todo", "blocked"]).optional().describe("Starting column \u2014 defaults to 'todo'"),
|
|
1871
|
+
dependsOn: z2.string().optional().describe(
|
|
1872
|
+
"Single parent card ID this card is stacked on \u2014 it continues in the parent's worktree and starts once the parent reaches ready_for_review. Mutually exclusive with waitsFor."
|
|
1873
|
+
),
|
|
1874
|
+
waitsFor: z2.array(z2.string()).optional().describe(
|
|
1875
|
+
"Card IDs this card waits for \u2014 it starts in a fresh worktree from the base branch only once ALL of them are done. Mutually exclusive with dependsOn."
|
|
1876
|
+
),
|
|
1877
|
+
workflowId: z2.string().min(1).describe(
|
|
1878
|
+
"REQUIRED. ID of the workflow this task runs. Call kanban_get_workflows first, then pick the workflow whose name/purpose best matches the task (e.g. a frontend workflow for UI-only work, a backend workflow for API work). Only fall back to the default task workflow if none is a good fit."
|
|
1879
|
+
),
|
|
1880
|
+
activeLevel: z2.enum(["minimal", "low", "medium", "high", "max"]).optional().describe(
|
|
1881
|
+
"Capability level the whole pipeline runs at (each slot maps it to its own model via the slot's pairs + mode). Higher = more capable/expensive. Omit to default to the workflow's highest configured tier; lower it for trivial work."
|
|
1882
|
+
),
|
|
1883
|
+
attachments: z2.array(attachmentInputSchema).optional().describe(
|
|
1884
|
+
"Files to attach (e.g. screenshots, design docs, PDFs). Reference each one inline in the description as [Attachment #N], where N is its 1-based position in this array (first \u2192 [Attachment #1])."
|
|
1885
|
+
),
|
|
1886
|
+
branchName: z2.string().min(1).describe(
|
|
1887
|
+
"REQUIRED. Git branch name for this card, derived from the title: '<type>/<slug>' (e.g. 'fix/user-auth-bug', 'feat/dark-mode'). All lowercase, dashes not underscores, \u226460 chars."
|
|
1888
|
+
)
|
|
1889
|
+
}
|
|
1890
|
+
},
|
|
1891
|
+
async ({
|
|
1892
|
+
description,
|
|
1893
|
+
type,
|
|
1894
|
+
priority,
|
|
1895
|
+
readyForDev,
|
|
1896
|
+
columnId,
|
|
1897
|
+
dependsOn,
|
|
1898
|
+
waitsFor,
|
|
1899
|
+
workflowId,
|
|
1900
|
+
activeLevel,
|
|
1901
|
+
attachments,
|
|
1902
|
+
branchName
|
|
1903
|
+
}) => {
|
|
1904
|
+
const card = await apiMutate("cards.create", {
|
|
1905
|
+
workspaceId,
|
|
1906
|
+
description,
|
|
1907
|
+
type,
|
|
1908
|
+
priority,
|
|
1909
|
+
readyForDev,
|
|
1910
|
+
dependsOn,
|
|
1911
|
+
waitsFor,
|
|
1912
|
+
columnId: columnId ?? "todo",
|
|
1913
|
+
workflowId,
|
|
1914
|
+
activeLevel,
|
|
1915
|
+
branchName: branchName || void 0
|
|
1916
|
+
});
|
|
1917
|
+
if (attachments?.length) {
|
|
1918
|
+
const processed = await processAttachments(attachments, card.id);
|
|
1919
|
+
if (processed.length) {
|
|
1920
|
+
await apiMutate("cards.update", {
|
|
1921
|
+
workspaceId,
|
|
1922
|
+
cardId: card.id,
|
|
1923
|
+
descriptionAttachments: processed,
|
|
1924
|
+
revision: 0
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
const cardDisplay = description.split("\n")[0]?.slice(0, 80) || card.id;
|
|
1929
|
+
return {
|
|
1930
|
+
content: [{ type: "text", text: `Created card [${card.id}] "${cardDisplay}" in ${card.columnId}.` }]
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
);
|
|
1934
|
+
registerTool(
|
|
1935
|
+
"kanban_move_card",
|
|
1936
|
+
{
|
|
1937
|
+
description: "Move a card to a different column.",
|
|
1938
|
+
inputSchema: {
|
|
1939
|
+
cardId: z2.string().describe("The card ID (from kanban_get_board)"),
|
|
1940
|
+
targetColumnId: z2.enum(["todo", "in_progress", "reopened", "ready_for_review", "blocked", "done"]).describe("Destination column")
|
|
1941
|
+
}
|
|
1942
|
+
},
|
|
1943
|
+
async ({ cardId, targetColumnId }) => {
|
|
1944
|
+
await apiMutate("cards.move", { workspaceId, cardId, targetColumnId, revision: 0 });
|
|
1945
|
+
return { content: [{ type: "text", text: `Moved card ${cardId} to ${targetColumnId}.` }] };
|
|
1946
|
+
}
|
|
1947
|
+
);
|
|
1948
|
+
registerTool(
|
|
1949
|
+
"kanban_update_card",
|
|
1950
|
+
{
|
|
1951
|
+
description: "Update a card's description, priority, dependencies, workflow, readyForDev flag, or attachments.",
|
|
1952
|
+
inputSchema: {
|
|
1953
|
+
cardId: z2.string().describe("The card ID"),
|
|
1954
|
+
description: z2.string().optional().describe("New description"),
|
|
1955
|
+
priority: z2.enum(["urgent", "high", "medium", "low"]).optional().describe("New priority level"),
|
|
1956
|
+
dependsOn: z2.string().optional().describe(
|
|
1957
|
+
"Single parent card ID this card is stacked on (replaces existing). Mutually exclusive with waitsFor."
|
|
1958
|
+
),
|
|
1959
|
+
waitsFor: z2.array(z2.string()).optional().describe(
|
|
1960
|
+
"Card IDs this card waits for \u2014 starts only once all are done (replaces existing). Mutually exclusive with dependsOn."
|
|
1961
|
+
),
|
|
1962
|
+
workflowId: z2.string().optional().describe("ID of the workflow to assign to this card"),
|
|
1963
|
+
readyForDev: z2.boolean().optional().describe("Whether the card is ready for the agent to pick up automatically"),
|
|
1964
|
+
activeLevel: z2.enum(["minimal", "low", "medium", "high", "max"]).optional().describe(
|
|
1965
|
+
"Capability level the pipeline runs at (each slot maps it to its own model). Raise for bigger scope, lower for trivial work. Omit to leave unchanged."
|
|
1966
|
+
),
|
|
1967
|
+
attachments: z2.array(attachmentInputSchema).optional().describe(
|
|
1968
|
+
"New files to append to the card's existing description attachments. Reference each one inline in the description as [Attachment #N], continuing the numbering after the card's existing attachments."
|
|
1969
|
+
)
|
|
1970
|
+
}
|
|
1971
|
+
},
|
|
1972
|
+
async ({ cardId, description, priority, dependsOn, waitsFor, workflowId, readyForDev, activeLevel, attachments }) => {
|
|
1973
|
+
let descriptionAttachments;
|
|
1974
|
+
if (attachments?.length) {
|
|
1975
|
+
const state = await apiQuery("workspace.state", { workspaceId });
|
|
1976
|
+
const existing = state.board.cards[cardId]?.descriptionAttachments ?? [];
|
|
1977
|
+
const processed = await processAttachments(attachments, cardId);
|
|
1978
|
+
descriptionAttachments = [...existing, ...processed];
|
|
1979
|
+
}
|
|
1980
|
+
await apiMutate("cards.update", {
|
|
1981
|
+
workspaceId,
|
|
1982
|
+
cardId,
|
|
1983
|
+
description,
|
|
1984
|
+
priority,
|
|
1985
|
+
dependsOn,
|
|
1986
|
+
waitsFor,
|
|
1987
|
+
workflowId,
|
|
1988
|
+
readyForDev,
|
|
1989
|
+
activeLevel,
|
|
1990
|
+
descriptionAttachments,
|
|
1991
|
+
revision: 0
|
|
1992
|
+
});
|
|
1993
|
+
return { content: [{ type: "text", text: `Updated card ${cardId}.` }] };
|
|
1994
|
+
}
|
|
1995
|
+
);
|
|
1996
|
+
registerTool(
|
|
1997
|
+
"kanban_add_comment",
|
|
1998
|
+
{
|
|
1999
|
+
description: "Record your analysis, findings, or summary as a comment on a Kanban card. Call this when you have finished your work so your output is cleanly stored.",
|
|
2000
|
+
inputSchema: {
|
|
2001
|
+
cardId: z2.string().describe("The card ID you are reviewing"),
|
|
2002
|
+
type: z2.string().describe("Type of comment \u2014 use the slot id for review agents (e.g. 'security_review')"),
|
|
2003
|
+
streamId: z2.string().optional().describe("The terminal session stream ID for this agent run"),
|
|
2004
|
+
summary: z2.string().describe("Your summary \u2014 2-5 sentences describing what you did or found"),
|
|
2005
|
+
status: z2.enum(["pass", "fail", "warning", "skipped"]).optional().describe("Result status of this review step"),
|
|
2006
|
+
issues: z2.array(
|
|
2007
|
+
z2.object({
|
|
2008
|
+
file: z2.string().optional().describe("File path where the issue was found"),
|
|
2009
|
+
line: z2.number().optional().describe("Line number of the issue"),
|
|
2010
|
+
severity: z2.enum(["blocking", "warning", "info"]).describe("Severity level"),
|
|
2011
|
+
message: z2.string().describe("Description of the issue")
|
|
2012
|
+
})
|
|
2013
|
+
).optional().describe("Specific issues found during review"),
|
|
2014
|
+
attachments: z2.array(attachmentInputSchema).optional().describe("File attachments (e.g. screenshots, PDFs)"),
|
|
2015
|
+
metadata: z2.record(z2.string(), z2.unknown()).optional().describe("Additional metadata key-value pairs"),
|
|
2016
|
+
suggestedLevel: z2.enum(["minimal", "low", "medium", "high", "max"]).optional().describe(
|
|
2017
|
+
"Only for review slots allowed to adjust the tier: the capability level the rework should run at when you fail/reopen (applies to all agents). Omit to leave unchanged."
|
|
2018
|
+
)
|
|
2019
|
+
}
|
|
2020
|
+
},
|
|
2021
|
+
async ({ cardId, type, streamId, summary, status, issues, attachments, metadata, suggestedLevel }) => {
|
|
2022
|
+
const processedAttachments = attachments?.length ? await processAttachments(attachments, cardId) : void 0;
|
|
2023
|
+
const mergedMetadata = suggestedLevel ? { ...metadata ?? {}, suggestedLevel } : metadata;
|
|
2024
|
+
await apiMutate("cards.addReviewComment", {
|
|
2025
|
+
workspaceId,
|
|
2026
|
+
cardId,
|
|
2027
|
+
type,
|
|
2028
|
+
streamId,
|
|
2029
|
+
actor: { type: "ai", id: agentId },
|
|
2030
|
+
status,
|
|
2031
|
+
summary,
|
|
2032
|
+
issues,
|
|
2033
|
+
attachments: processedAttachments,
|
|
2034
|
+
metadata: mergedMetadata
|
|
2035
|
+
});
|
|
2036
|
+
return { content: [{ type: "text", text: `Comment recorded on card ${cardId}.` }] };
|
|
2037
|
+
}
|
|
2038
|
+
);
|
|
2039
|
+
registerTool(
|
|
2040
|
+
"kanban_set_plan",
|
|
2041
|
+
{
|
|
2042
|
+
description: "Save an implementation plan onto a card. Called by the plan agent when it finishes; the dev agent then reads this plan to implement the task.",
|
|
2043
|
+
inputSchema: {
|
|
2044
|
+
cardId: z2.string().describe("The card ID to save the plan on"),
|
|
2045
|
+
plan: z2.string().describe("The implementation plan \u2014 files to change, approach, edge cases, verification steps")
|
|
2046
|
+
}
|
|
2047
|
+
},
|
|
2048
|
+
async ({ cardId, plan }) => {
|
|
2049
|
+
await apiMutate("cards.setPlan", { workspaceId, cardId, plan });
|
|
2050
|
+
return { content: [{ type: "text", text: `Plan saved on card ${cardId}.` }] };
|
|
2051
|
+
}
|
|
2052
|
+
);
|
|
2053
|
+
registerTool(
|
|
2054
|
+
"kanban_delete_card",
|
|
2055
|
+
{
|
|
2056
|
+
description: "Delete a card from the board permanently.",
|
|
2057
|
+
inputSchema: {
|
|
2058
|
+
cardId: z2.string().describe("The card ID to delete")
|
|
2059
|
+
}
|
|
2060
|
+
},
|
|
2061
|
+
async ({ cardId }) => {
|
|
2062
|
+
await apiMutate("cards.delete", { workspaceId, cardId });
|
|
2063
|
+
return { content: [{ type: "text", text: `Deleted card ${cardId}.` }] };
|
|
2064
|
+
}
|
|
2065
|
+
);
|
|
2066
|
+
registerTool(
|
|
2067
|
+
"kanban_stop_task",
|
|
2068
|
+
{
|
|
2069
|
+
description: "Stop an in-progress agent task. The session is marked 'stopped' (preserving history) so the card can be restarted later. Use this before moving a child card to todo when its parent was reopened.",
|
|
2070
|
+
inputSchema: {
|
|
2071
|
+
cardId: z2.string().describe("The card ID of the in-progress task to stop")
|
|
2072
|
+
}
|
|
2073
|
+
},
|
|
2074
|
+
async ({ cardId }) => {
|
|
2075
|
+
await apiMutate("cards.interruptTask", { workspaceId, cardId });
|
|
2076
|
+
return { content: [{ type: "text", text: `Task ${cardId} interrupted.` }] };
|
|
2077
|
+
}
|
|
2078
|
+
);
|
|
2079
|
+
registerTool(
|
|
2080
|
+
"kanban_get_workflows",
|
|
2081
|
+
{
|
|
2082
|
+
description: "Get all workflows configured for this project, including their agent slots, model tiers, tools, and prompts.",
|
|
2083
|
+
inputSchema: {}
|
|
2084
|
+
},
|
|
2085
|
+
async () => {
|
|
2086
|
+
const workflows = await apiQuery("workflows.list", { workspaceId });
|
|
2087
|
+
const lines = [];
|
|
2088
|
+
for (const wf of workflows) {
|
|
2089
|
+
const kind = wf.forStory ? " [story/orch workflow]" : " [task workflow]";
|
|
2090
|
+
lines.push(`## ${wf.name}${wf.isDefault ? " (default)" : ""}${kind} [id: ${wf.id}]`);
|
|
2091
|
+
const sorted = [...wf.slots].sort((a, b) => a.order - b.order);
|
|
2092
|
+
for (const slot of sorted) {
|
|
2093
|
+
const status = slot.enabled ? "enabled" : "disabled";
|
|
2094
|
+
const top = slot.pairs[0];
|
|
2095
|
+
const topTag = top ? `, top: ${top.binary}${top.model ? `/${top.model}` : ""}@${top.level}${top.effort ? `/${top.effort}` : ""}` : "";
|
|
2096
|
+
const pairsTag = `, ${slot.pairs.length} tier(s), mode: ${slot.mode}`;
|
|
2097
|
+
const toolsTag = slot.tools.length ? `, tools: ${slot.tools.join("+")}` : "";
|
|
2098
|
+
const flags = [slot.canAdjustLevel ? "canAdjustLevel" : "", slot.rerun ? "rerun" : ""].filter(Boolean).join(",");
|
|
2099
|
+
const flagsTag = flags ? `, ${flags}` : "";
|
|
2100
|
+
const promptText = slot.prompt && typeof slot.prompt === "object" ? slot.prompt.source === "inline" ? slot.prompt.text : `[file: ${slot.prompt.path}]` : "";
|
|
2101
|
+
const prompt = promptText ? `
|
|
2102
|
+
prompt: ${promptText}` : "";
|
|
2103
|
+
lines.push(
|
|
2104
|
+
` - [${slot.id}] ${slot.name} (${slot.type}${topTag}${pairsTag}, ${status}${toolsTag}${flagsTag})${prompt}`
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
return { content: [{ type: "text", text: lines.join("\n") || "No workflows configured." }] };
|
|
2109
|
+
}
|
|
2110
|
+
);
|
|
2111
|
+
registerTool(
|
|
2112
|
+
"kanban_upsert_workflow",
|
|
2113
|
+
{
|
|
2114
|
+
description: "Create or update a workflow. Pass the full workflow object including all slots. If a workflow with the given id already exists it will be replaced; otherwise a new one is created.",
|
|
2115
|
+
inputSchema: {
|
|
2116
|
+
id: z2.string().describe("Unique workflow ID. Use a short slug like 'wf_security' for new workflows."),
|
|
2117
|
+
name: z2.string().describe("Human-readable workflow name, e.g. 'Security Review'"),
|
|
2118
|
+
isDefault: z2.boolean().optional().describe("Whether this is the default workflow (only one can be default)"),
|
|
2119
|
+
forStory: z2.boolean().optional().describe(
|
|
2120
|
+
"True for story/orchestrator workflows (orch slots only). False for regular task workflows (plan/dev/review slots)."
|
|
2121
|
+
),
|
|
2122
|
+
slots: z2.array(
|
|
2123
|
+
z2.object({
|
|
2124
|
+
id: z2.string().describe("Unique slot ID within this workflow"),
|
|
2125
|
+
type: z2.enum(["dev", "review", "plan", "orch"]).describe(
|
|
2126
|
+
"Slot type. 'dev' implements (only slot with write access). 'review' is a one-shot reviewer \u2014 chain several via order, grant tools as needed. 'plan' runs once before dev and saves a plan on the card. 'orch' is story-only."
|
|
2127
|
+
),
|
|
2128
|
+
name: z2.string().describe("Display name for this slot"),
|
|
2129
|
+
order: z2.number().int().nonnegative().describe("Execution order (0 = first)"),
|
|
2130
|
+
enabled: z2.boolean().describe("Whether this slot is active in the pipeline"),
|
|
2131
|
+
prompt: z2.string().describe("System prompt / instructions for this agent slot. Empty string for default behavior."),
|
|
2132
|
+
pairs: z2.array(
|
|
2133
|
+
z2.object({
|
|
2134
|
+
id: z2.string().describe("Unique pair ID within this slot"),
|
|
2135
|
+
level: z2.enum(["minimal", "low", "medium", "high", "max"]).describe("Capability level. The card's active level selects which pair runs."),
|
|
2136
|
+
isFree: z2.boolean().describe("Whether this pair uses a zero-cost model"),
|
|
2137
|
+
binary: z2.enum(["claude", "codex", "opencode", "cursor"]).describe("Agent binary: claude / codex / opencode / cursor."),
|
|
2138
|
+
model: z2.string().nullable().optional().describe("Model override, or null for the agent default (e.g. 'claude-opus-4-6', 'gpt-5.5')."),
|
|
2139
|
+
effort: z2.enum(["low", "medium", "high", "xhigh", "max"]).nullable().optional().describe("Reasoning effort override, or null for the agent default.")
|
|
2140
|
+
})
|
|
2141
|
+
).min(1).describe(
|
|
2142
|
+
"Model tiers for this slot, in priority order (first = highest). The card copies these and picks one by active level + mode."
|
|
2143
|
+
),
|
|
2144
|
+
mode: z2.enum(["auto", "preferFree", "freeOnly", "paidOnly"]).optional().describe(
|
|
2145
|
+
"Selection policy at the active level: auto = top priority; preferFree = top free else paid; freeOnly / paidOnly = restrict by cost. Defaults to auto."
|
|
2146
|
+
),
|
|
2147
|
+
tools: z2.array(z2.enum(["browser"])).optional().describe("Tools granted to this slot, e.g. ['browser'] for Playwright UI control."),
|
|
2148
|
+
canAdjustLevel: z2.boolean().optional().describe("review slots only: may set the card's active level on reopen via suggestedLevel."),
|
|
2149
|
+
rerun: z2.boolean().optional().describe("plan slots only: re-run the plan even if the card already has one.")
|
|
2150
|
+
})
|
|
2151
|
+
).describe(
|
|
2152
|
+
"For task workflows: always include a dev slot (type: 'dev'). Add plan and/or review slots as needed. For story workflows: use only orch slots."
|
|
2153
|
+
)
|
|
2154
|
+
}
|
|
2155
|
+
},
|
|
2156
|
+
async ({ id, name, isDefault, forStory, slots }) => {
|
|
2157
|
+
const workflow = await apiMutate("workflows.upsert", {
|
|
2158
|
+
workspaceId,
|
|
2159
|
+
workflow: { id, name, isDefault: isDefault ?? false, forStory: forStory ?? false, slots }
|
|
2160
|
+
});
|
|
2161
|
+
return { content: [{ type: "text", text: `Workflow "${workflow.name}" [${workflow.id}] saved successfully.` }] };
|
|
2162
|
+
}
|
|
2163
|
+
);
|
|
2164
|
+
registerTool(
|
|
2165
|
+
"kanban_get_pr_meta",
|
|
2166
|
+
{
|
|
2167
|
+
description: "Get the current PR metadata stored on a card \u2014 url (set by the daemon when the PR is created), title, description, and the updatedAt/updatedBy stamps from the last write. Use this when the dev prompt shows a truncated previous description and you need the full text to revise.",
|
|
2168
|
+
inputSchema: {
|
|
2169
|
+
cardId: z2.string().describe("The card ID to read PR metadata from")
|
|
2170
|
+
}
|
|
2171
|
+
},
|
|
2172
|
+
async ({ cardId }) => {
|
|
2173
|
+
const state = await apiQuery("workspace.state", { workspaceId });
|
|
2174
|
+
const card = state.board.cards[cardId];
|
|
2175
|
+
if (!card) {
|
|
2176
|
+
return {
|
|
2177
|
+
content: [
|
|
2178
|
+
{ type: "text", text: `Card ${cardId} not found on the board. Use kanban_get_board to list valid card IDs.` }
|
|
2179
|
+
]
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
const pr = card.pr;
|
|
2183
|
+
if (!pr || !pr.url && !pr.title && !pr.description) {
|
|
2184
|
+
return { content: [{ type: "text", text: `No PR metadata on card ${cardId} yet.` }] };
|
|
2185
|
+
}
|
|
2186
|
+
const lines = [
|
|
2187
|
+
`url: ${pr.url ?? "(not yet created)"}`,
|
|
2188
|
+
`title: ${pr.title ?? "(unset)"}`,
|
|
2189
|
+
"",
|
|
2190
|
+
"description:",
|
|
2191
|
+
pr.description ?? "(unset)"
|
|
2192
|
+
];
|
|
2193
|
+
if (pr.updatedAt) lines.push("", `last updated: ${new Date(pr.updatedAt).toISOString()} by ${pr.updatedBy ?? "?"}`);
|
|
2194
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2195
|
+
}
|
|
2196
|
+
);
|
|
2197
|
+
registerTool(
|
|
2198
|
+
"kanban_set_pr_meta",
|
|
2199
|
+
{
|
|
2200
|
+
description: "Set the PR title and/or description for a card. Call this at the end of your work so the daemon uses your text when it creates the PR. The PR url is set by the daemon and never overwritten by this call. Subsequent calls overwrite previous values \u2014 revise rather than rewrite when prior values already reflect the change.",
|
|
2201
|
+
inputSchema: {
|
|
2202
|
+
cardId: z2.string().describe("The card ID this PR is for"),
|
|
2203
|
+
title: z2.string().optional().describe("PR title. Follow the project's git instructions."),
|
|
2204
|
+
description: z2.string().optional().describe("PR description body. Follow the project's git instructions."),
|
|
2205
|
+
streamId: z2.string().optional().describe("Terminal session stream ID for this agent run, if available")
|
|
2206
|
+
}
|
|
2207
|
+
},
|
|
2208
|
+
async ({ cardId, title, description, streamId }) => {
|
|
2209
|
+
const result = await apiMutate("cards.setPrMeta", {
|
|
2210
|
+
workspaceId,
|
|
2211
|
+
cardId,
|
|
2212
|
+
title,
|
|
2213
|
+
description,
|
|
2214
|
+
updatedBy: streamId ?? agentId
|
|
2215
|
+
});
|
|
2216
|
+
const parts = [];
|
|
2217
|
+
if (result.pr.title) parts.push(`title="${result.pr.title}"`);
|
|
2218
|
+
if (result.pr.description) parts.push(`description (${result.pr.description.length} chars)`);
|
|
2219
|
+
return {
|
|
2220
|
+
content: [{ type: "text", text: `PR meta updated on card ${cardId}: ${parts.join(", ") || "(empty)"}.` }]
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
);
|
|
2224
|
+
registerTool(
|
|
2225
|
+
"kanban_get_git_instructions",
|
|
2226
|
+
{
|
|
2227
|
+
description: "Get the project's git conventions for PR titles, PR descriptions, and commit messages. Returns the user's custom override (if any), the built-in default, and the effective text that the dev prompt actually injects. Read this before writing PR meta so you follow project conventions.",
|
|
2228
|
+
inputSchema: {}
|
|
2229
|
+
},
|
|
2230
|
+
async () => {
|
|
2231
|
+
const config = await apiQuery("projectConfig.get", { workspaceId });
|
|
2232
|
+
const custom = config.gitInstructions?.trim() ? config.gitInstructions : null;
|
|
2233
|
+
const effective = custom ?? DEFAULT_GIT_INSTRUCTIONS;
|
|
2234
|
+
const lines = [];
|
|
2235
|
+
lines.push(
|
|
2236
|
+
custom ? "## Custom git instructions (project override)" : "## Custom git instructions: (none \u2014 using default)"
|
|
2237
|
+
);
|
|
2238
|
+
if (custom) lines.push(custom);
|
|
2239
|
+
lines.push("");
|
|
2240
|
+
lines.push("## Default git instructions");
|
|
2241
|
+
lines.push(DEFAULT_GIT_INSTRUCTIONS);
|
|
2242
|
+
lines.push("");
|
|
2243
|
+
lines.push("## Effective (what dev agents actually see)");
|
|
2244
|
+
lines.push(effective);
|
|
2245
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2246
|
+
}
|
|
2247
|
+
);
|
|
2248
|
+
registerTool(
|
|
2249
|
+
"kanban_set_git_instructions",
|
|
2250
|
+
{
|
|
2251
|
+
description: "Replace the project's custom git conventions (PR title/description/commit rules). This is project-wide and affects all future PRs in this workspace \u2014 only call when explicitly asked to change conventions, not as part of regular task work. Pass an empty string to clear the override and revert to the built-in default.",
|
|
2252
|
+
inputSchema: {
|
|
2253
|
+
instructions: z2.string().describe(
|
|
2254
|
+
"Full replacement text in markdown or freeform prose. Pass an empty string to clear the override and use the default."
|
|
2255
|
+
)
|
|
2256
|
+
}
|
|
2257
|
+
},
|
|
2258
|
+
async ({ instructions }) => {
|
|
2259
|
+
const result = await apiMutate("projectConfig.setGitInstructions", {
|
|
2260
|
+
workspaceId,
|
|
2261
|
+
instructions
|
|
2262
|
+
});
|
|
2263
|
+
return {
|
|
2264
|
+
content: [
|
|
2265
|
+
{
|
|
2266
|
+
type: "text",
|
|
2267
|
+
text: result.cleared ? "Custom git instructions cleared \u2014 using default." : "Custom git instructions updated."
|
|
2268
|
+
}
|
|
2269
|
+
]
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
);
|
|
2273
|
+
registerTool(
|
|
2274
|
+
"kanban_get_system_prompt",
|
|
2275
|
+
{
|
|
2276
|
+
description: "Get the shared system prompt for this project. This prompt is appended to every agent \u2014 plan, dev, review, and the assistant chat.",
|
|
2277
|
+
inputSchema: {}
|
|
2278
|
+
},
|
|
2279
|
+
async () => {
|
|
2280
|
+
const config = await apiQuery("projectConfig.get", { workspaceId });
|
|
2281
|
+
const prompt = config.systemPrompt?.trim();
|
|
2282
|
+
return { content: [{ type: "text", text: prompt ? prompt : "(no shared system prompt set)" }] };
|
|
2283
|
+
}
|
|
2284
|
+
);
|
|
2285
|
+
registerTool(
|
|
2286
|
+
"kanban_set_system_prompt",
|
|
2287
|
+
{
|
|
2288
|
+
description: "Set or update the shared system prompt for this project. The prompt is appended to every agent \u2014 plan, dev, review, and the assistant chat. Pass an empty string to clear it.",
|
|
2289
|
+
inputSchema: {
|
|
2290
|
+
prompt: z2.string().describe("The new shared system prompt. Pass an empty string to clear the existing prompt.")
|
|
2291
|
+
}
|
|
2292
|
+
},
|
|
2293
|
+
async ({ prompt }) => {
|
|
2294
|
+
const result = await apiMutate("projectConfig.setSystemPrompt", {
|
|
2295
|
+
workspaceId,
|
|
2296
|
+
prompt
|
|
2297
|
+
});
|
|
2298
|
+
return {
|
|
2299
|
+
content: [
|
|
2300
|
+
{ type: "text", text: result.cleared ? `Shared system prompt cleared.` : `Shared system prompt updated.` }
|
|
2301
|
+
]
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2304
|
+
);
|
|
2305
|
+
registerTool(
|
|
2306
|
+
"task_complete",
|
|
2307
|
+
{
|
|
2308
|
+
description: "Signal that you have finished all work on this task. Call this ONLY after you have completed all code changes, called kanban_set_pr_meta, and called kanban_add_comment with your final status. This terminates the agent session.",
|
|
2309
|
+
inputSchema: {}
|
|
2310
|
+
},
|
|
2311
|
+
async () => {
|
|
2312
|
+
const taskId = process.env.WHIPPED_HOOK_TASK_ID;
|
|
2313
|
+
const wsId = process.env.WHIPPED_HOOK_WORKSPACE_ID;
|
|
2314
|
+
if (taskId && wsId) {
|
|
2315
|
+
await fetch(
|
|
2316
|
+
`${serverUrl}/api/hook?event=stop&taskId=${encodeURIComponent(taskId)}&workspaceId=${encodeURIComponent(wsId)}`,
|
|
2317
|
+
{ headers: authHeaders }
|
|
2318
|
+
).catch(() => {
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
return { content: [{ type: "text", text: "Task completion signaled. Your session will now end." }] };
|
|
2322
|
+
}
|
|
2323
|
+
);
|
|
2324
|
+
var agentSlot = process.env.WHIPPED_SLOT ?? "";
|
|
2325
|
+
var memoryCardId = process.env.WHIPPED_HOOK_TASK_ID ?? "";
|
|
2326
|
+
var memoryModel = process.env.WHIPPED_MODEL ?? "";
|
|
2327
|
+
registerTool(
|
|
2328
|
+
"whipped_search_memory",
|
|
2329
|
+
{
|
|
2330
|
+
description: "Search durable project + global memory (conventions, decisions, lessons, sharp edges) for this workspace. Use before re-discovering how something works. Returns matching memories with their ids.",
|
|
2331
|
+
inputSchema: {
|
|
2332
|
+
query: z2.string().describe("Keywords to search memory titles and content")
|
|
2333
|
+
}
|
|
2334
|
+
},
|
|
2335
|
+
async ({ query }) => {
|
|
2336
|
+
try {
|
|
2337
|
+
const results = await apiQuery("memory.search", { query, workspaceId });
|
|
2338
|
+
if (!results || results.length === 0) {
|
|
2339
|
+
return { content: [{ type: "text", text: "No matching memory." }] };
|
|
2340
|
+
}
|
|
2341
|
+
const text = results.map((m) => `- [${m.id}] (${m.scope}/${m.type}) ${m.title}
|
|
2342
|
+
${m.content}`).join("\n");
|
|
2343
|
+
return { content: [{ type: "text", text }] };
|
|
2344
|
+
} catch (err) {
|
|
2345
|
+
return { content: [{ type: "text", text: `Search failed: ${err.message}` }] };
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
);
|
|
2349
|
+
registerTool(
|
|
2350
|
+
"whipped_get_memory",
|
|
2351
|
+
{
|
|
2352
|
+
description: "Fetch the full content of a single memory by its id (from whipped_search_memory results).",
|
|
2353
|
+
inputSchema: {
|
|
2354
|
+
id: z2.string().describe("Memory id")
|
|
2355
|
+
}
|
|
2356
|
+
},
|
|
2357
|
+
async ({ id }) => {
|
|
2358
|
+
try {
|
|
2359
|
+
const m = await apiQuery("memory.get", { id });
|
|
2360
|
+
if (!m) return { content: [{ type: "text", text: "Memory not found." }] };
|
|
2361
|
+
return { content: [{ type: "text", text: `(${m.scope}/${m.type}) ${m.title}
|
|
2362
|
+
|
|
2363
|
+
${m.content}` }] };
|
|
2364
|
+
} catch (err) {
|
|
2365
|
+
return { content: [{ type: "text", text: `Fetch failed: ${err.message}` }] };
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
);
|
|
2369
|
+
if (agentSlot === "dev" || agentSlot === "assistant") {
|
|
2370
|
+
registerTool(
|
|
2371
|
+
"whipped_save_memory",
|
|
2372
|
+
{
|
|
2373
|
+
description: "Save a durable memory so future agents don't re-discover it. Use for conventions, architecture facts, decisions, gotchas, or user corrections worth remembering across tasks. Scope 'project' = specific to this repo; 'global' = a fact shareable across projects (e.g. a framework/library convention). Keep each memory to one focused fact. The user may review project task-lessons; global lessons go to a review queue.\n\nGlobal memory REQUIRES tags \u2014 it only reaches another project that subscribes to one of its tags. Tag with the framework-qualified form when the knowledge is ecosystem-specific, and the bare form only when the fact is truly tool-level and framework-agnostic: Spoosh used via its React bindings \u2192 'spoosh-react'; a fact about Spoosh itself \u2192 'spoosh'; React hooks \u2192 'react-hook' (not bare 'hook'). Reuse an existing tag from the injected memory's tag list before inventing a near-duplicate.",
|
|
2374
|
+
inputSchema: {
|
|
2375
|
+
scope: z2.enum(["project", "global"]).describe("'project' (this repo) or 'global' (shareable across projects)"),
|
|
2376
|
+
type: z2.enum(["fact", "convention", "decision", "preference", "rule", "lesson", "sharp_edge"]).describe("Kind of memory"),
|
|
2377
|
+
title: z2.string().describe("Short one-line summary"),
|
|
2378
|
+
content: z2.string().describe("The durable fact, in 1-3 sentences"),
|
|
2379
|
+
sourceType: z2.enum(["user_correction", "explicit_save", "task_lesson"]).default("task_lesson").describe("Why this is being saved \u2014 'user_correction' if the user explicitly told you, else 'task_lesson'"),
|
|
2380
|
+
importance: z2.number().int().min(1).max(3).optional().describe("1 normal, 2 high, 3 critical"),
|
|
2381
|
+
tags: z2.array(z2.string()).optional().describe(
|
|
2382
|
+
"Required for 'global' scope (\u22651). Canonical kebab-case, framework-qualified when ecosystem-specific (e.g. 'spoosh-react', 'react-hook'). Reuse existing tags."
|
|
2383
|
+
)
|
|
2384
|
+
}
|
|
2385
|
+
},
|
|
2386
|
+
async ({ scope, type, title, content, sourceType, importance, tags }) => {
|
|
2387
|
+
try {
|
|
2388
|
+
const saved = await apiMutate("memory.propose", {
|
|
2389
|
+
scope,
|
|
2390
|
+
workspaceId: scope === "project" ? workspaceId : void 0,
|
|
2391
|
+
originWorkspaceId: workspaceId,
|
|
2392
|
+
type,
|
|
2393
|
+
title,
|
|
2394
|
+
content,
|
|
2395
|
+
sourceType,
|
|
2396
|
+
importance,
|
|
2397
|
+
tags,
|
|
2398
|
+
originCardId: memoryCardId || void 0,
|
|
2399
|
+
originAgent: { agent: agentId, ...memoryModel ? { model: memoryModel } : {} }
|
|
2400
|
+
});
|
|
2401
|
+
const note = saved.status === "approved" ? "saved (approved)." : "submitted for the user's review (pending).";
|
|
2402
|
+
return { content: [{ type: "text", text: `Memory ${note}` }] };
|
|
2403
|
+
} catch (err) {
|
|
2404
|
+
return { content: [{ type: "text", text: `Save failed: ${err.message}` }] };
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
);
|
|
2408
|
+
registerTool(
|
|
2409
|
+
"whipped_update_memory",
|
|
2410
|
+
{
|
|
2411
|
+
description: "Update an existing memory when it is now inaccurate or out of date (e.g. a convention changed). Get the memory's id from the injected memory list or whipped_search_memory. Prefer updating over saving a near-duplicate. Approval follows the same policy as saving.",
|
|
2412
|
+
inputSchema: {
|
|
2413
|
+
id: z2.string().describe("Memory id (from the memory list or whipped_search_memory)"),
|
|
2414
|
+
title: z2.string().optional().describe("New title"),
|
|
2415
|
+
content: z2.string().optional().describe("New content"),
|
|
2416
|
+
type: z2.enum(["fact", "convention", "decision", "preference", "rule", "lesson", "sharp_edge"]).optional(),
|
|
2417
|
+
importance: z2.number().int().min(1).max(3).optional(),
|
|
2418
|
+
sourceType: z2.enum(["user_correction", "explicit_save", "task_lesson"]).default("task_lesson").describe("'user_correction' if the user told you to change it, else 'task_lesson'")
|
|
2419
|
+
}
|
|
2420
|
+
},
|
|
2421
|
+
async ({ id, title, content, type, importance, sourceType }) => {
|
|
2422
|
+
try {
|
|
2423
|
+
const updated = await apiMutate("memory.proposeUpdate", {
|
|
2424
|
+
id,
|
|
2425
|
+
title,
|
|
2426
|
+
content,
|
|
2427
|
+
type,
|
|
2428
|
+
importance,
|
|
2429
|
+
sourceType
|
|
2430
|
+
});
|
|
2431
|
+
const note = updated.status === "approved" ? "updated (approved)." : "update submitted for the user's review (pending).";
|
|
2432
|
+
return { content: [{ type: "text", text: `Memory ${note}` }] };
|
|
2433
|
+
} catch (err) {
|
|
2434
|
+
return { content: [{ type: "text", text: `Update failed: ${err.message}` }] };
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
);
|
|
2438
|
+
}
|
|
2439
|
+
var scheduleShape = {
|
|
2440
|
+
scheduleKind: z2.enum(["interval", "calendar"]).describe("'interval' = every N seconds; 'calendar' = cron at a wall-clock time"),
|
|
2441
|
+
intervalSeconds: z2.number().int().positive().optional().describe("Required when scheduleKind=interval (e.g. 3600 = hourly)"),
|
|
2442
|
+
cronExpr: z2.string().optional().describe("Required when scheduleKind=calendar, e.g. '0 9 * * 1' = every Monday 09:00"),
|
|
2443
|
+
timezone: z2.string().optional().describe("IANA timezone, required when scheduleKind=calendar (e.g. 'Asia/Yangon')"),
|
|
2444
|
+
agentBinary: z2.enum(["claude", "codex", "opencode", "cursor"]).optional().describe("Which agent runs this; defaults to claude"),
|
|
2445
|
+
model: z2.string().optional().describe("Model id, e.g. 'claude-opus-4-8' or 'gpt-5.5'"),
|
|
2446
|
+
effort: z2.enum(["low", "medium", "high", "xhigh", "max"]).optional(),
|
|
2447
|
+
enabled: z2.boolean().optional()
|
|
2448
|
+
};
|
|
2449
|
+
function buildScheduleBody(input) {
|
|
2450
|
+
const schedule = input.scheduleKind === "calendar" ? { kind: "calendar", cronExpr: input.cronExpr ?? "0 9 * * 1", timezone: input.timezone ?? "UTC" } : { kind: "interval", intervalSeconds: input.intervalSeconds ?? 3600 };
|
|
2451
|
+
const model = input.agentBinary ? { agentId: input.agentBinary, model: input.model ?? null, effort: input.effort ?? null } : void 0;
|
|
2452
|
+
return { schedule, model };
|
|
2453
|
+
}
|
|
2454
|
+
if (mcpRole === "assistant") {
|
|
2455
|
+
registerTool(
|
|
2456
|
+
"recurring_create",
|
|
2457
|
+
{
|
|
2458
|
+
description: "Create a scheduled recurring agent for this project (e.g. an hourly Jira checker or a weekly security sweep). Only you, the assistant, can create these \u2014 recurring agents cannot create others. They observe and report; they do not write code.",
|
|
2459
|
+
inputSchema: {
|
|
2460
|
+
name: z2.string().describe("Short name, e.g. 'Security sweep'"),
|
|
2461
|
+
instructions: z2.string().describe("What the agent should do each run, and how to use its journal"),
|
|
2462
|
+
...scheduleShape
|
|
2463
|
+
}
|
|
2464
|
+
},
|
|
2465
|
+
async (input) => {
|
|
2466
|
+
try {
|
|
2467
|
+
const { schedule, model } = buildScheduleBody(input);
|
|
2468
|
+
const created = await apiMutate("recurring.create", {
|
|
2469
|
+
workspaceId,
|
|
2470
|
+
name: input.name,
|
|
2471
|
+
instructions: input.instructions,
|
|
2472
|
+
schedule,
|
|
2473
|
+
model,
|
|
2474
|
+
enabled: input.enabled
|
|
2475
|
+
});
|
|
2476
|
+
return { content: [{ type: "text", text: `Created recurring agent "${created.name}" [${created.id}].` }] };
|
|
2477
|
+
} catch (err) {
|
|
2478
|
+
return { content: [{ type: "text", text: `Create failed: ${err.message}` }] };
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
);
|
|
2482
|
+
registerTool(
|
|
2483
|
+
"recurring_list",
|
|
2484
|
+
{ description: "List the recurring agents configured for this project.", inputSchema: {} },
|
|
2485
|
+
async () => {
|
|
2486
|
+
const agents = await apiQuery("recurring.list", { workspaceId });
|
|
2487
|
+
if (!agents.length) return { content: [{ type: "text", text: "No recurring agents configured." }] };
|
|
2488
|
+
const lines = agents.map(
|
|
2489
|
+
(a) => `[${a.id}] ${a.name} \u2014 ${a.enabled ? "enabled" : "disabled"}, ${a.schedule.kind}${a.nextRunAt ? `, next ${new Date(a.nextRunAt).toISOString()}` : ""}`
|
|
2490
|
+
);
|
|
2491
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2492
|
+
}
|
|
2493
|
+
);
|
|
2494
|
+
registerTool(
|
|
2495
|
+
"recurring_update",
|
|
2496
|
+
{
|
|
2497
|
+
description: "Update a recurring agent (rename, re-schedule, change model, enable/disable).",
|
|
2498
|
+
inputSchema: {
|
|
2499
|
+
id: z2.string(),
|
|
2500
|
+
name: z2.string().optional(),
|
|
2501
|
+
instructions: z2.string().optional(),
|
|
2502
|
+
...scheduleShape
|
|
2503
|
+
}
|
|
2504
|
+
},
|
|
2505
|
+
async (input) => {
|
|
2506
|
+
try {
|
|
2507
|
+
const body = { id: input.id, name: input.name, instructions: input.instructions };
|
|
2508
|
+
if (input.scheduleKind) {
|
|
2509
|
+
const { schedule, model } = buildScheduleBody(input);
|
|
2510
|
+
body.schedule = schedule;
|
|
2511
|
+
if (model) body.model = model;
|
|
2512
|
+
}
|
|
2513
|
+
if (input.enabled !== void 0) body.enabled = input.enabled;
|
|
2514
|
+
await apiMutate("recurring.update", body);
|
|
2515
|
+
return { content: [{ type: "text", text: `Updated recurring agent ${input.id}.` }] };
|
|
2516
|
+
} catch (err) {
|
|
2517
|
+
return { content: [{ type: "text", text: `Update failed: ${err.message}` }] };
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
);
|
|
2521
|
+
registerTool(
|
|
2522
|
+
"recurring_delete",
|
|
2523
|
+
{ description: "Delete a recurring agent.", inputSchema: { id: z2.string() } },
|
|
2524
|
+
async ({ id }) => {
|
|
2525
|
+
try {
|
|
2526
|
+
await apiMutate("recurring.delete", { id });
|
|
2527
|
+
return { content: [{ type: "text", text: `Deleted recurring agent ${id}.` }] };
|
|
2528
|
+
} catch (err) {
|
|
2529
|
+
return { content: [{ type: "text", text: `Delete failed: ${err.message}` }] };
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
if (mcpRole === "recurring" && recurringAgentId) {
|
|
2535
|
+
registerTool(
|
|
2536
|
+
"update_journal",
|
|
2537
|
+
{
|
|
2538
|
+
description: "Rewrite your private journal \u2014 the notes you carry across runs (e.g. what you've already filed, what you're watching). Read it at the start of a run and rewrite the full updated text here at the end. This replaces the journal entirely, so include everything worth keeping.",
|
|
2539
|
+
inputSchema: { journal: z2.string().describe("The full updated journal text") }
|
|
2540
|
+
},
|
|
2541
|
+
async ({ journal }) => {
|
|
2542
|
+
try {
|
|
2543
|
+
await apiMutate("recurring.setJournal", { id: recurringAgentId, journal });
|
|
2544
|
+
return { content: [{ type: "text", text: "Journal saved." }] };
|
|
2545
|
+
} catch (err) {
|
|
2546
|
+
return { content: [{ type: "text", text: `Journal save failed: ${err.message}` }] };
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
var transport = new StdioServerTransport();
|
|
2552
|
+
await server.connect(transport);
|