whipped 0.3.0 → 0.5.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/dist/cli.js CHANGED
@@ -3529,10 +3529,11 @@ function isResumableSessionState(state) {
3529
3529
  function normalizeTag(raw2) {
3530
3530
  return raw2.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3531
3531
  }
3532
- 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;
3532
+ var ASSISTANT_AGENT_PREFIX, 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, runtimeBulkCardImportItemSchema, runtimeBulkCardsCreateRequestSchema, runtimeCardMoveRequestSchema, runtimeCardUpdateRequestSchema, memoryScopeSchema, memoryTypeSchema, memorySourceTypeSchema, memoryStatusSchema, runtimeMemoryOriginAgentSchema, runtimeMemorySchema, recurringScheduleKindSchema, recurringScheduleSchema, recurringRunStatusSchema, recurringRunTriggerSchema, recurringAgentRunSchema, recurringAgentSchema, recurringAgentCreateRequestSchema, recurringAgentUpdateRequestSchema, projectFolderSchema, topLevelItemSchema, projectsLayoutSchema, runtimeProjectSchema;
3533
3533
  var init_api_contract = __esm({
3534
3534
  "src/core/api-contract.ts"() {
3535
3535
  "use strict";
3536
+ ASSISTANT_AGENT_PREFIX = "__assistant__:";
3536
3537
  runtimeAgentIdSchema = z.enum(["claude", "codex", "opencode", "cursor", "mimo"]);
3537
3538
  effortLevelSchema = z.enum(["low", "medium", "high", "xhigh", "max"]);
3538
3539
  agentModelChoiceSchema = z.object({
@@ -3961,6 +3962,14 @@ Do NOT include:
3961
3962
  modelConfig: cardModelConfigSchema.optional(),
3962
3963
  activeLevel: tierLevelSchema.optional()
3963
3964
  });
3965
+ runtimeBulkCardImportItemSchema = runtimeCardCreateRequestSchema.extend({
3966
+ tempId: z.string().optional()
3967
+ });
3968
+ runtimeBulkCardsCreateRequestSchema = z.object({
3969
+ // Batch-wide base branch; an item's own baseRef overrides it, else resolved server-side.
3970
+ baseRef: z.string().optional(),
3971
+ cards: z.array(runtimeBulkCardImportItemSchema).min(1)
3972
+ });
3964
3973
  runtimeCardMoveRequestSchema = z.object({
3965
3974
  cardId: z.string(),
3966
3975
  targetColumnId: runtimeBoardColumnIdSchema,
@@ -8000,6 +8009,7 @@ __export(workspace_state_exports, {
8000
8009
  clearCardSession: () => clearCardSession,
8001
8010
  closeAllOpenTerminalSessions: () => closeAllOpenTerminalSessions,
8002
8011
  createCard: () => createCard,
8012
+ createCardsBulk: () => createCardsBulk,
8003
8013
  deleteCard: () => deleteCard,
8004
8014
  downloadGithubImages: () => downloadGithubImages,
8005
8015
  endTerminalSession: () => endTerminalSession,
@@ -8550,6 +8560,79 @@ async function createCard(workspaceId, data, baseRef) {
8550
8560
  tx();
8551
8561
  return card;
8552
8562
  }
8563
+ async function createCardsBulk(workspaceId, items, baseRef) {
8564
+ const db = getDb();
8565
+ const now = Date.now();
8566
+ const projectConfig = loadProjectConfigInternal(workspaceId);
8567
+ const realIds = items.map(() => generateTaskId());
8568
+ const tempIdToRealId = /* @__PURE__ */ new Map();
8569
+ items.forEach((item, i) => {
8570
+ if (item.tempId) tempIdToRealId.set(item.tempId, realIds[i]);
8571
+ });
8572
+ const resolveRef = (ref) => tempIdToRealId.get(ref) ?? ref;
8573
+ const cards = items.map((item, i) => {
8574
+ const type = item.type ?? "task";
8575
+ const columnId = item.columnId ?? "todo";
8576
+ const workflow = resolveWorkflowForCard(projectConfig.workflows, { workflowId: item.workflowId, type });
8577
+ const waitsFor = (item.waitsFor ?? []).map(resolveRef);
8578
+ const dependsOn = item.dependsOn ? resolveRef(item.dependsOn) : void 0;
8579
+ return {
8580
+ id: realIds[i],
8581
+ description: item.description,
8582
+ columnId,
8583
+ type,
8584
+ readyForDev: item.readyForDev ?? type === "story",
8585
+ agentId: item.agentId,
8586
+ priority: item.priority,
8587
+ // dependsOn (stacking) and waitsFor (gate) are mutually exclusive — waitsFor wins.
8588
+ dependsOn: waitsFor.length > 0 ? void 0 : dependsOn,
8589
+ waitsFor,
8590
+ subtaskIds: (item.subtaskIds ?? []).map(resolveRef),
8591
+ autoFixAttempts: 0,
8592
+ activeLevel: item.activeLevel ?? highestWorkflowLevel(workflow),
8593
+ modelConfig: item.modelConfig ?? snapshotModelConfig(workflow),
8594
+ baseRef: item.baseRef ?? baseRef,
8595
+ createdAt: now,
8596
+ updatedAt: now,
8597
+ githubIssueUrl: item.githubIssueUrl,
8598
+ workflowId: item.workflowId ?? workflow?.id,
8599
+ descriptionAttachments: item.descriptionAttachments ?? [],
8600
+ branchName: item.branchName,
8601
+ reviewComments: [],
8602
+ activityLog: [],
8603
+ terminalSessions: [],
8604
+ githubCommentIds: []
8605
+ };
8606
+ });
8607
+ const columnCounts = /* @__PURE__ */ new Map();
8608
+ const nextPosition = (columnId) => {
8609
+ if (!columnCounts.has(columnId)) {
8610
+ const row = db.prepare("SELECT COUNT(*) AS n FROM cards WHERE workspace_id = ? AND column_id = ?").get(workspaceId, columnId);
8611
+ columnCounts.set(columnId, row.n);
8612
+ }
8613
+ const pos = columnCounts.get(columnId);
8614
+ columnCounts.set(columnId, pos + 1);
8615
+ return pos;
8616
+ };
8617
+ const tx = db.transaction(() => {
8618
+ for (const card of cards) {
8619
+ upsertCardRow(db, workspaceId, { ...card, dependsOn: void 0 }, nextPosition(card.columnId));
8620
+ }
8621
+ const setDep = db.prepare("UPDATE cards SET depends_on_id = ? WHERE id = ?");
8622
+ for (const card of cards) {
8623
+ if (card.dependsOn && db.prepare("SELECT 1 FROM cards WHERE id = ?").get(card.dependsOn)) {
8624
+ setDep.run(card.dependsOn, card.id);
8625
+ } else if (card.dependsOn) {
8626
+ card.dependsOn = void 0;
8627
+ }
8628
+ replaceCardWaitsFor(db, card.id, card.waitsFor ?? []);
8629
+ replaceCardSubtasks(db, card.id, card.subtaskIds ?? []);
8630
+ }
8631
+ bumpBoardRevision(db, workspaceId);
8632
+ });
8633
+ tx();
8634
+ return cards;
8635
+ }
8553
8636
  async function appendActivityLog(workspaceId, cardId, message) {
8554
8637
  const db = getDb();
8555
8638
  const tx = db.transaction(() => {
@@ -14307,6 +14390,7 @@ init_logger();
14307
14390
  init_task_id();
14308
14391
 
14309
14392
  // src/daemon/poller.ts
14393
+ init_runtime_config();
14310
14394
  init_logger();
14311
14395
  init_task_id();
14312
14396
  import { spawnSync as spawnSync3 } from "node:child_process";
@@ -18655,6 +18739,8 @@ var BoardPoller = class {
18655
18739
  async poll() {
18656
18740
  const { workspaceId, repoPath, scheduler, stateHub, onCardReadyForReview } = this.options;
18657
18741
  const state = await loadWorkspaceState(workspaceId, repoPath);
18742
+ const effectiveLimit = state.projectConfig.maxParallelTasks ?? (await loadGlobalConfig()).maxParallelTasks;
18743
+ scheduler.setMaxParallelTasks(effectiveLimit);
18658
18744
  const board = state.board;
18659
18745
  const pendingCards = [];
18660
18746
  const todoColumn = board.columns.find((c) => c.id === "todo");
@@ -18806,9 +18892,6 @@ var BoardPoller = class {
18806
18892
  await moveCard(workspaceId, taskId, "reopened");
18807
18893
  await appendActivityLog(workspaceId, taskId, `${reason} \u2192 Reopened`);
18808
18894
  await clearCardSession(workspaceId, taskId);
18809
- const refreshedBoard = await loadBoard(workspaceId);
18810
- const refreshedCard = refreshedBoard.cards[taskId] ?? card;
18811
- void scheduler.triggerParentReopenCascade(refreshedCard, refreshedBoard.cards);
18812
18895
  updated = true;
18813
18896
  }
18814
18897
  if (updated) stateHub.broadcastWorkspaceUpdate(workspaceId);
@@ -19074,6 +19157,7 @@ function buildAgentArgs(agentId, prompt, ctx = {}) {
19074
19157
  case "mimo": {
19075
19158
  if (mode === "print") {
19076
19159
  const args2 = ["run", "--agent", "build"];
19160
+ if (agentId === "mimo") args2.push("--never-ask", "--trust");
19077
19161
  if (ctx.model) args2.push("-m", ctx.model);
19078
19162
  if (ctx.effort) {
19079
19163
  const OPENCODE_EFFORT_VARIANT = {
@@ -19089,6 +19173,7 @@ function buildAgentArgs(agentId, prompt, ctx = {}) {
19089
19173
  return args2;
19090
19174
  }
19091
19175
  const args = ["--agent", "build"];
19176
+ if (agentId === "mimo") args.push("--never-ask", "--trust");
19092
19177
  if (ctx.model) args.push("-m", ctx.model);
19093
19178
  if (prompt.trim()) args.push("--prompt", prompt);
19094
19179
  return args;
@@ -20010,38 +20095,6 @@ function markRecurringRan(recurringAgentId) {
20010
20095
  getDb().prepare("UPDATE recurring_agents SET last_run_at = ?, next_run_at = ? WHERE id = ?").run(now, nextRunAt, recurringAgentId);
20011
20096
  }
20012
20097
 
20013
- // src/daemon/recurring-agent-scheduler.ts
20014
- init_workspace_state();
20015
- init_workspace_state();
20016
-
20017
- // src/daemon/scheduler.ts
20018
- import { spawn as spawn5 } from "node:child_process";
20019
- import { cpSync, existsSync as existsSync10, mkdirSync as mkdirSync9 } from "node:fs";
20020
- import { unlink as unlink2 } from "node:fs/promises";
20021
- import { dirname as dirname5, join as join16, resolve as resolve2 } from "node:path";
20022
- import { fileURLToPath as fileURLToPath3 } from "node:url";
20023
- init_api_contract();
20024
- init_logger();
20025
-
20026
- // src/core/prompt-resolver.ts
20027
- init_logger();
20028
- import { readFileSync as readFileSync5 } from "node:fs";
20029
- import { isAbsolute, join as join14 } from "node:path";
20030
- function resolvePromptText(prompt, repoPath) {
20031
- if (!prompt) return "";
20032
- if (prompt.source === "inline") return prompt.text;
20033
- const path2 = isAbsolute(prompt.path) ? prompt.path : join14(repoPath, prompt.path);
20034
- try {
20035
- return readFileSync5(path2, "utf-8");
20036
- } catch (err) {
20037
- logger.warn({ err: err.message, path: path2 }, "Slot prompt file unreadable \u2014 falling back to empty prompt");
20038
- return "";
20039
- }
20040
- }
20041
-
20042
- // src/daemon/scheduler.ts
20043
- init_task_id();
20044
-
20045
20098
  // src/state/memory-store.ts
20046
20099
  init_api_contract();
20047
20100
  init_task_id();
@@ -20159,6 +20212,7 @@ function createMemory(input) {
20159
20212
  throw new Error("global-scoped memory requires at least one tag");
20160
20213
  }
20161
20214
  const originWorkspaceId = input.originWorkspaceId ?? workspaceId;
20215
+ const originCardId = input.originCardId && db.prepare("SELECT 1 FROM cards WHERE id = ?").get(input.originCardId) ? input.originCardId : null;
20162
20216
  const tx = db.transaction(() => {
20163
20217
  db.prepare(
20164
20218
  `INSERT INTO memories (
@@ -20175,7 +20229,7 @@ function createMemory(input) {
20175
20229
  input.content,
20176
20230
  input.sourceType,
20177
20231
  input.importance ?? 1,
20178
- input.originCardId ?? null,
20232
+ originCardId,
20179
20233
  input.originAgent ? JSON.stringify(input.originAgent) : null,
20180
20234
  input.status ?? "approved",
20181
20235
  now,
@@ -20299,7 +20353,7 @@ function searchMemories(query, workspaceId, limit = 20) {
20299
20353
  ).all({ q: ftsQuery, ws: workspaceId, limit });
20300
20354
  return hydrate(rows.map(rowToMemory));
20301
20355
  }
20302
- function buildMemoryContext(workspaceId, memoryLimit = 40) {
20356
+ function buildMemoryContext(workspaceId, memoryLimit = 40, opts) {
20303
20357
  const sections = [];
20304
20358
  const fmt = (m2) => {
20305
20359
  const tagSuffix = m2.tags.length > 0 ? ` _(tags: ${m2.tags.join(", ")})_` : "";
@@ -20317,18 +20371,50 @@ ${projectMem.map(fmt).join("\n")}`);
20317
20371
  ${globalMem.map(fmt).join("\n")}`);
20318
20372
  }
20319
20373
  if (sections.length === 0) return "";
20374
+ const readOnly = opts?.readOnly ?? false;
20320
20375
  const knownTags = listTags();
20321
- const tagLine = knownTags.length > 0 ? `
20376
+ const tagLine = !readOnly && knownTags.length > 0 ? `
20322
20377
 
20323
20378
  Existing tags (reuse before inventing new ones): ${knownTags.join(", ")}.` : "";
20379
+ const recallLine = readOnly ? "Use `whipped_search_memory` / `whipped_get_memory` to recall more." : "Use `whipped_search_memory` / `whipped_get_memory` to recall more, and `whipped_update_memory` to correct an entry that's now wrong.";
20324
20380
  return [
20325
20381
  "## Memory",
20326
- `This is whipped's persistent project memory \u2014 durable knowledge from past work. Each entry is prefixed with its id. Use \`whipped_search_memory\` / \`whipped_get_memory\` to recall more, and \`whipped_update_memory\` to correct an entry that's now wrong. Treat these as hints, not gospel: if a memory references a file, symbol, or rule, verify it still holds before relying on it.${tagLine}`,
20382
+ `This is whipped's persistent project memory \u2014 durable knowledge from past work. Each entry is prefixed with its id. ${recallLine} Treat these as hints, not gospel: if a memory references a file, symbol, or rule, verify it still holds before relying on it.${tagLine}`,
20327
20383
  ...sections
20328
20384
  ].join("\n\n");
20329
20385
  }
20330
20386
 
20387
+ // src/daemon/recurring-agent-scheduler.ts
20388
+ init_workspace_state();
20389
+ init_workspace_state();
20390
+
20331
20391
  // src/daemon/scheduler.ts
20392
+ import { spawn as spawn5 } from "node:child_process";
20393
+ import { cpSync, existsSync as existsSync10, mkdirSync as mkdirSync9 } from "node:fs";
20394
+ import { unlink as unlink2 } from "node:fs/promises";
20395
+ import { dirname as dirname5, join as join16, resolve as resolve2 } from "node:path";
20396
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
20397
+ init_api_contract();
20398
+ init_logger();
20399
+
20400
+ // src/core/prompt-resolver.ts
20401
+ init_logger();
20402
+ import { readFileSync as readFileSync5 } from "node:fs";
20403
+ import { isAbsolute, join as join14 } from "node:path";
20404
+ function resolvePromptText(prompt, repoPath) {
20405
+ if (!prompt) return "";
20406
+ if (prompt.source === "inline") return prompt.text;
20407
+ const path2 = isAbsolute(prompt.path) ? prompt.path : join14(repoPath, prompt.path);
20408
+ try {
20409
+ return readFileSync5(path2, "utf-8");
20410
+ } catch (err) {
20411
+ logger.warn({ err: err.message, path: path2 }, "Slot prompt file unreadable \u2014 falling back to empty prompt");
20412
+ return "";
20413
+ }
20414
+ }
20415
+
20416
+ // src/daemon/scheduler.ts
20417
+ init_task_id();
20332
20418
  init_workspace_state();
20333
20419
 
20334
20420
  // src/daemon/review-pipeline.ts
@@ -21583,128 +21669,10 @@ function tryParseAgentJson(output) {
21583
21669
  return null;
21584
21670
  }
21585
21671
  }
21586
- async function runParentReopenCascade(parentCard, childCards, options) {
21587
- const { workspaceId, repoPath, mcpBinary, serverUrl, stateHub, secrets, registerStopCallback, registerLiveProcess } = options;
21588
- const streamId = `${parentCard.id}-cascade-${Date.now()}`;
21589
- const mcpConfigPath = getMcpConfigPath(streamId);
21590
- await writeClaudeMcpConfig(mcpBinary, serverUrl, workspaceId, "claude", mcpConfigPath).catch(() => {
21591
- });
21592
- const parentBranch = getCardBranch(parentCard);
21593
- const systemPrompt = buildCascadeSystemPrompt(parentCard, parentBranch, childCards);
21594
- logger.info(
21595
- `[cascade] Spawning cascade agent for parent "${parentCard.description?.split("\n")[0]?.slice(0, 60) ?? parentCard.id}" (${childCards.length} children)`
21596
- );
21597
- await appendTerminalSession(workspaceId, parentCard.id, {
21598
- streamId,
21599
- type: "cascade",
21600
- startedAt: Date.now(),
21601
- state: "running"
21602
- });
21603
- stateHub.broadcastWorkspaceUpdate(workspaceId);
21604
- await runAgentOnce(
21605
- "claude",
21606
- "Evaluate each child ticket and take the appropriate action.",
21607
- repoPath,
21608
- workspaceId,
21609
- streamId,
21610
- stateHub,
21611
- registerStopCallback,
21612
- registerLiveProcess,
21613
- mcpConfigPath,
21614
- systemPrompt,
21615
- void 0,
21616
- buildSecretsEnv(secrets),
21617
- "low"
21618
- );
21619
- const cascadeStopped = options.isStreamManuallyStopped(streamId);
21620
- logger.info(`[cascade:${streamId}] Cascade agent done \u2014 manuallyStopped=${cascadeStopped}`);
21621
- await endTerminalSession(workspaceId, parentCard.id, streamId, Date.now(), cascadeStopped ? "stopped" : "completed");
21622
- if (cascadeStopped) return;
21623
- stateHub.broadcastWorkspaceUpdate(workspaceId);
21624
- if (options.onChildReset) {
21625
- const afterBoard = await loadBoard(workspaceId);
21626
- const resetChildren = childCards.filter((child) => afterBoard.cards[child.id]?.columnId === "todo");
21627
- for (const child of resetChildren) {
21628
- logger.info(
21629
- `[cascade] Recursing into reset child "${child.description?.split("\n")[0]?.slice(0, 60) ?? child.id}"`
21630
- );
21631
- await options.onChildReset(child);
21632
- }
21633
- }
21634
- }
21635
- function buildCascadeSystemPrompt(parentCard, parentBranch, childCards) {
21636
- const comments = parentCard.reviewComments ?? [];
21637
- const iterations = groupIntoIterations(parentCard);
21638
- const current = iterations[iterations.length - 1];
21639
- const currentInputSummaries = current?.input.map((c) => c.summary).filter(Boolean) ?? [];
21640
- const reopenReason = currentInputSummaries.length > 0 ? currentInputSummaries.join("\n") : "Parent task was reopened.";
21641
- const allDevSummaries = comments.filter((c) => c.type === "dev").map((c, i) => `Dev iteration ${i + 1}:
21642
- ${c.summary}`).join("\n\n");
21643
- const childLines = childCards.map((child) => {
21644
- const devComment = [...child.reviewComments ?? []].reverse().find((c) => c.type === "dev");
21645
- const desc = child.description ? `
21646
- ${child.description}
21647
- ` : "";
21648
- return [
21649
- `### [${child.id}] (${child.columnId})`,
21650
- desc,
21651
- devComment ? `**Dev summary:** ${devComment.summary}` : "No dev work completed yet."
21652
- ].filter(Boolean).join("\n");
21653
- }).join("\n\n");
21654
- return `You are a Kanban board manager. A parent task was reopened and you must decide what to do with its dependent child tasks.
21655
-
21656
- All data you need is already provided below \u2014 do NOT call \`kanban_get_board\`. Proceed directly to taking action.
21657
-
21658
-
21659
- ## Parent Task (Reopened)
21660
-
21661
- **[${parentCard.id}]**
21662
- ${parentCard.description ? `
21663
- ${parentCard.description}
21664
- ` : ""}
21665
- **Reason for reopening (= the parent's new direction, not yet implemented):** ${reopenReason}
21666
- ${allDevSummaries ? `
21667
- Parent's full dev history (OLD state \u2014 do NOT use this to judge conflicts, use the reopening reason above):
21668
- ${allDevSummaries}
21669
- ` : ""}
21670
-
21671
- ## Child Tasks to Evaluate
21672
-
21673
- ${childLines}
21674
-
21675
- ## Decision Rules
21676
-
21677
- CRITICAL: The cascade runs BEFORE the parent's dev agent implements the feedback. The parent's existing dev summary reflects its OLD state \u2014 do NOT use it to evaluate conflicts. Use ONLY the **reason for reopening** (the human feedback) to determine what the parent is about to change. That is the parent's new direction.
21678
-
21679
- **Reset a child when:**
21680
- - The reopening reason describes a change that directly conflicts with the child's purpose or existing work
21681
- - e.g. reason is "Remove username field", child's purpose is "Add username field" \u2192 direct conflict \u2192 reset
21682
- - e.g. reason is "Change the API response shape", child is building a UI that consumes that API \u2192 reset
21683
-
21684
- **Leave a child alone when:**
21685
- - The reopening reason describes a change to something completely unrelated to the child's purpose
21686
- - e.g. reason is "Remove email field", child's purpose is "Add username field" \u2192 unrelated \u2192 leave alone
21687
- - e.g. reason is "Fix a bug in the payment module", child is working on user profiles \u2192 leave alone
21688
-
21689
- The default is to **leave children alone**. Only reset when the reopening reason directly conflicts with what the child is doing.
21690
-
21691
- ## Steps for EACH child you decide to reset
21692
-
21693
- 1. Call \`kanban_stop_task\` if the child is in_progress.
21694
- 2. Call \`kanban_add_comment\` on the **CHILD** card with:
21695
- - type: "cascade"
21696
- - status: "fail"
21697
- - summary: Explain specifically what the parent changed and why this child's prior work needs to be revisited.
21698
- - issues: include one blocking issue with severity "blocking" and message: "Run \`git merge ${parentBranch}\` \u2014 no fetch needed since all task worktrees in this project share the same local repo. After merging, implement this task's original goal on top of the parent's new state. The parent's changes are the new baseline \u2014 build on them, do not mirror them."
21699
- 3. Call \`kanban_move_card\` to "todo" for that child.
21700
-
21701
- After handling all children, call \`kanban_add_comment\` on the PARENT card (${parentCard.id}) with type "cascade" and a brief summary of each decision.`;
21702
- }
21703
21672
 
21704
21673
  // src/daemon/scheduler.ts
21705
21674
  var FAST_EXIT_THRESHOLD_MS = 8e3;
21706
21675
  var MAX_RECENT_BUFFERS = 100;
21707
- var ASSISTANT_AGENT_PREFIX = "__assistant__:";
21708
21676
  var TaskScheduler = class {
21709
21677
  constructor(options) {
21710
21678
  this.options = options;
@@ -21729,10 +21697,8 @@ var TaskScheduler = class {
21729
21697
  manuallyStoppedForHook = /* @__PURE__ */ new Set();
21730
21698
  // Tasks stopped before the dev agent started (e.g. during plan phase).
21731
21699
  planPhaseManuallyStopped = /* @__PURE__ */ new Set();
21732
- // Individual review/cascade stream IDs stopped by a manual stopTask() call.
21700
+ // Individual review stream IDs stopped by a manual stopTask() call.
21733
21701
  manuallyStoppedStreams = /* @__PURE__ */ new Set();
21734
- // Tasks stopped because their parent was reopened — session set to "stopped" in onExit.
21735
- parentReopenedTasks = /* @__PURE__ */ new Set();
21736
21702
  // Shared worktree IDs currently in use by a dev agent — prevents sibling cards from
21737
21703
  // running concurrently in the same worktree directory.
21738
21704
  runningSharedWorktrees = /* @__PURE__ */ new Set();
@@ -21872,6 +21838,11 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
21872
21838
  get maxParallelTasks() {
21873
21839
  return this.options.maxParallelTasks;
21874
21840
  }
21841
+ // Config can change at runtime; the poller re-syncs this each tick so a
21842
+ // changed "Max Parallel Tasks" takes effect without restarting the daemon.
21843
+ setMaxParallelTasks(limit) {
21844
+ this.options.maxParallelTasks = limit;
21845
+ }
21875
21846
  canAcceptTask(inFlightCount) {
21876
21847
  const count = inFlightCount ?? this.running.size;
21877
21848
  return count < this.options.maxParallelTasks;
@@ -22221,12 +22192,6 @@ ${devSystemPromptResult.text}`;
22221
22192
  stateHub.broadcastWorkspaceUpdate(workspaceId);
22222
22193
  return;
22223
22194
  }
22224
- if (this.parentReopenedTasks.has(taskId)) {
22225
- this.parentReopenedTasks.delete(taskId);
22226
- await endTerminalSession(workspaceId, taskId, devStreamId, Date.now(), "stopped");
22227
- stateHub.broadcastWorkspaceUpdate(workspaceId);
22228
- return;
22229
- }
22230
22195
  if (this.hookHandledTasks.has(taskId)) {
22231
22196
  this.hookHandledTasks.delete(taskId);
22232
22197
  const exitedAt2 = Date.now();
@@ -22392,45 +22357,6 @@ ${devSystemPromptResult.text}`;
22392
22357
  isStreamManuallyStopped(streamId) {
22393
22358
  return this.manuallyStoppedStreams.delete(streamId);
22394
22359
  }
22395
- // Stop a task because its parent was reopened — session becomes "stopped" rather than being removed.
22396
- interruptForParentReopen(taskId) {
22397
- const task = this.running.get(taskId);
22398
- if (task) {
22399
- logger.info(`[scheduler] Interrupting task ${taskId} due to parent reopen`);
22400
- this.setRecentBuffer(task.streamId, task.outputBuffer);
22401
- void saveTerminalBuffer(this.options.workspaceId, task.streamId, task.outputBuffer);
22402
- this.parentReopenedTasks.add(taskId);
22403
- task.process.kill();
22404
- this.running.delete(taskId);
22405
- }
22406
- }
22407
- async triggerParentReopenCascade(parentCard, boardCards) {
22408
- if (parentCard.type !== "task") return;
22409
- const { workspaceId, repoPath, serverUrl, stateHub } = this.options;
22410
- const childCards = Object.values(boardCards).filter(
22411
- (card) => card.dependsOn?.includes(parentCard.id) && (card.columnId === "in_progress" || card.columnId === "ready_for_review")
22412
- );
22413
- if (childCards.length === 0) return;
22414
- logger.info(
22415
- `[scheduler] triggerParentReopenCascade: ${childCards.length} children for parent "${parentCard.description?.split("\n")[0]?.slice(0, 60) ?? parentCard.id}"`
22416
- );
22417
- const projectConfig = await loadProjectConfig(workspaceId);
22418
- void runParentReopenCascade(parentCard, childCards, {
22419
- workspaceId,
22420
- repoPath,
22421
- serverUrl,
22422
- mcpBinary: getMcpServerPath(),
22423
- stateHub,
22424
- secrets: projectConfig.secrets ?? [],
22425
- registerStopCallback: this.registerStopCallback.bind(this),
22426
- registerLiveProcess: this.registerLiveProcess.bind(this),
22427
- isStreamManuallyStopped: this.isStreamManuallyStopped.bind(this),
22428
- onChildReset: async (child) => {
22429
- const latestBoard = await loadBoard(workspaceId);
22430
- await this.triggerParentReopenCascade(child, latestBoard.cards);
22431
- }
22432
- });
22433
- }
22434
22360
  getOutputBuffer(streamId) {
22435
22361
  for (const task of this.running.values()) {
22436
22362
  if (task.streamId === streamId) return task.outputBuffer;
@@ -22868,7 +22794,7 @@ var RecurringAgentScheduler = class {
22868
22794
  } catch (err) {
22869
22795
  logger.warn({ err, agentId: agent.id }, "[recurring] failed to load project config");
22870
22796
  }
22871
- const appendSystemPrompt = buildRecurringSystemPrompt(
22797
+ const baseSystemPrompt = buildRecurringSystemPrompt(
22872
22798
  repoPath,
22873
22799
  agent.name,
22874
22800
  agent.instructions,
@@ -22876,6 +22802,10 @@ var RecurringAgentScheduler = class {
22876
22802
  secrets,
22877
22803
  projectSystemPrompt
22878
22804
  );
22805
+ const memContext = buildMemoryContext(workspaceId, 40, { readOnly: true });
22806
+ const appendSystemPrompt = memContext ? `${memContext}
22807
+
22808
+ ${baseSystemPrompt}` : baseSystemPrompt;
22879
22809
  const prompt = buildRecurringPrompt();
22880
22810
  if (agentBinary === "claude" && mcpConfigPath) {
22881
22811
  await writeClaudeMcpConfig(
@@ -23006,8 +22936,10 @@ ${journalBlock}
23006
22936
 
23007
22937
  When you finish, call \`update_journal\` with the full updated notes to keep for next time (what you checked, what you filed, what you're watching). It REPLACES the journal, so include everything still relevant.
23008
22938
 
22939
+ If your task is a one-off that is now complete, or the condition you were watching for is resolved and there is nothing left to do on future runs, call \`disable_self\` to stop running on schedule. This only pauses future runs (the user can re-enable you); the current run still finishes normally. Update your journal before disabling.
22940
+
23009
22941
  ## Available tools
23010
- \`kanban_get_board\`, \`kanban_create_card\`, \`kanban_add_comment\`, \`kanban_get_workflows\`, \`whipped_search_memory\`, \`whipped_get_memory\`, \`update_journal\`, plus read-only repo tools (Read, Grep, Glob).`;
22942
+ \`kanban_get_board\`, \`kanban_create_card\`, \`kanban_add_comment\`, \`kanban_get_workflows\`, \`whipped_search_memory\`, \`whipped_get_memory\`, \`update_journal\`, \`disable_self\`, plus read-only repo tools (Read, Grep, Glob).`;
23011
22943
  const secretsSection = buildSecretsSection(secrets);
23012
22944
  if (secretsSection) prompt += `
23013
22945
 
@@ -25742,6 +25674,36 @@ Stack: ${err instanceof Error ? err.stack : ""}`
25742
25674
  throw err;
25743
25675
  }
25744
25676
  };
25677
+ var bulkCreateCardsService = async (workspaceId, items, requestedBase) => {
25678
+ const workspaces = await listWorkspaces();
25679
+ const ws = workspaces.find((w2) => w2.workspaceId === workspaceId);
25680
+ if (!ws) throw NotFoundError("Workspace");
25681
+ const config = await loadProjectConfig(workspaceId);
25682
+ if (config.workflows.filter((w2) => !w2.forStory).length === 0) {
25683
+ throw BadRequestError("Create at least one workflow before importing tickets.");
25684
+ }
25685
+ const hasStoryWorkflow = config.workflows.some((w2) => w2.forStory);
25686
+ const board = await loadBoard(workspaceId);
25687
+ const existingCardIds = new Set(Object.keys(board.cards));
25688
+ const tempIds = new Set(items.map((it) => it.tempId).filter((t) => Boolean(t)));
25689
+ const errors = [];
25690
+ items.forEach((item, index) => {
25691
+ if (!item.description?.trim()) errors.push({ index, message: "description is required" });
25692
+ if ((item.type === "story" || item.type === "subtask") && !hasStoryWorkflow) {
25693
+ errors.push({ index, message: "story/subtask tickets require a story workflow \u2014 create one first" });
25694
+ }
25695
+ const refs = [...item.dependsOn ? [item.dependsOn] : [], ...item.waitsFor ?? [], ...item.subtaskIds ?? []];
25696
+ for (const ref of refs) {
25697
+ if (!tempIds.has(ref) && !existingCardIds.has(ref)) {
25698
+ errors.push({ index, message: `unknown reference "${ref}"` });
25699
+ }
25700
+ }
25701
+ });
25702
+ if (errors.length > 0) throw BadRequestError("Import validation failed", errors);
25703
+ const baseRef = requestedBase || config.defaultBaseBranch || getDefaultBranch(ws.repoPath);
25704
+ const cards = await createCardsBulk(workspaceId, items, baseRef);
25705
+ return { cards };
25706
+ };
25745
25707
  var listBranchesService = async (workspaceId, remote = false) => {
25746
25708
  const workspaces = await listWorkspaces();
25747
25709
  const ws = workspaces.find((w2) => w2.workspaceId === workspaceId);
@@ -25946,11 +25908,6 @@ var moveCardService = async (workspaceId, cardId, targetColumnId, targetIndex) =
25946
25908
  }
25947
25909
  if (targetColumnId === "reopened") {
25948
25910
  await updateCard(workspaceId, cardId, { autoFixAttempts: 0 });
25949
- const movedBoard = await loadBoard(workspaceId);
25950
- const movedCard = movedBoard.cards[cardId];
25951
- if (movedCard) {
25952
- return { board, reopenCascade: { movedCard, boardCards: movedBoard.cards } };
25953
- }
25954
25911
  }
25955
25912
  return { board };
25956
25913
  };
@@ -26026,32 +25983,28 @@ var deleteReviewCommentService = async (input) => {
26026
25983
  await updateCard(input.workspaceId, input.cardId, { reviewComments: next });
26027
25984
  return { ok: true };
26028
25985
  };
26029
- var submitHumanFeedbackService = async (workspaceId, cardId, comment, attachments) => {
25986
+ var submitHumanFeedbackService = async (workspaceId, cardId, comment, attachments, type, metadata) => {
26030
25987
  const board = await loadBoard(workspaceId);
26031
25988
  const card = board.cards[cardId];
26032
25989
  if (!card) throw NotFoundError("Card");
26033
25990
  const trimmed = comment?.trim();
26034
- const hasContent = trimmed || (attachments?.length ?? 0) > 0;
25991
+ const hasContent = trimmed || (attachments?.length ?? 0) > 0 || metadata != null;
26035
25992
  const updatedComments = hasContent ? [
26036
25993
  ...card.reviewComments ?? [],
26037
25994
  {
26038
25995
  id: generateTaskId(),
26039
- type: "human",
25996
+ type: type ?? "human",
26040
25997
  actor: { type: "human", id: "human" },
26041
25998
  createdAt: Date.now(),
26042
25999
  summary: trimmed ?? "Feedback with attachments",
26043
- attachments: attachments?.length ? attachments : void 0
26000
+ attachments: attachments?.length ? attachments : void 0,
26001
+ ...metadata ? { metadata } : {}
26044
26002
  }
26045
26003
  ] : card.reviewComments ?? [];
26046
26004
  await updateCard(workspaceId, cardId, { reviewComments: updatedComments, autoFixAttempts: 0 });
26047
26005
  await moveCard(workspaceId, cardId, "reopened");
26048
26006
  await clearCardSession(workspaceId, cardId);
26049
26007
  await appendActivityLog(workspaceId, cardId, "Human feedback submitted \u2192 moved to Reopened");
26050
- const feedbackBoard = await loadBoard(workspaceId);
26051
- const feedbackCard = feedbackBoard.cards[cardId];
26052
- if (feedbackCard) {
26053
- return { ok: true, reopenCascade: { feedbackCard, boardCards: feedbackBoard.cards } };
26054
- }
26055
26008
  return { ok: true };
26056
26009
  };
26057
26010
  var prepareStartAgentService = async (workspaceId, cardId) => {
@@ -26194,6 +26147,12 @@ var cardsController = new Hono2().post("/", zv("json", runtimeCardCreateRequestS
26194
26147
  const card = await createCardService(workspaceId, cardData, baseRef);
26195
26148
  ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
26196
26149
  return c.json(card);
26150
+ }).post("/bulk", zv("json", runtimeBulkCardsCreateRequestSchema.extend({ workspaceId: z5.string() })), async (c) => {
26151
+ const ctx = c.var.ctx;
26152
+ const { workspaceId, baseRef, cards } = c.req.valid("json");
26153
+ const result = await bulkCreateCardsService(workspaceId, cards, baseRef);
26154
+ ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
26155
+ return c.json(result);
26197
26156
  }).get(
26198
26157
  "/branches",
26199
26158
  zv("query", z5.object({ workspaceId: z5.string(), remote: z5.enum(["true", "false"]).optional() })),
@@ -26286,12 +26245,6 @@ var cardsController = new Hono2().post("/", zv("json", runtimeCardCreateRequestS
26286
26245
  const ctx = c.var.ctx;
26287
26246
  const { workspaceId, cardId, targetColumnId, targetIndex } = c.req.valid("json");
26288
26247
  const result = await moveCardService(workspaceId, cardId, targetColumnId, targetIndex);
26289
- if (result.reopenCascade) {
26290
- const scheduler = ctx.getScheduler(workspaceId);
26291
- if (scheduler) {
26292
- void scheduler.triggerParentReopenCascade(result.reopenCascade.movedCard, result.reopenCascade.boardCards);
26293
- }
26294
- }
26295
26248
  ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
26296
26249
  return c.json(result.board);
26297
26250
  }).delete(
@@ -26353,20 +26306,16 @@ var cardsController = new Hono2().post("/", zv("json", runtimeCardCreateRequestS
26353
26306
  workspaceId: z5.string(),
26354
26307
  cardId: z5.string(),
26355
26308
  comment: z5.string().optional(),
26356
- attachments: z5.array(reviewAttachmentSchema).optional()
26309
+ attachments: z5.array(reviewAttachmentSchema).optional(),
26310
+ type: z5.string().optional(),
26311
+ metadata: z5.record(z5.string(), z5.unknown()).optional()
26357
26312
  })
26358
26313
  ),
26359
26314
  async (c) => {
26360
26315
  const ctx = c.var.ctx;
26361
- const { workspaceId, cardId, comment, attachments } = c.req.valid("json");
26362
- const result = await submitHumanFeedbackService(workspaceId, cardId, comment, attachments);
26316
+ const { workspaceId, cardId, comment, attachments, type, metadata } = c.req.valid("json");
26317
+ const result = await submitHumanFeedbackService(workspaceId, cardId, comment, attachments, type, metadata);
26363
26318
  ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
26364
- if (result.reopenCascade) {
26365
- const scheduler = ctx.getScheduler(workspaceId);
26366
- if (scheduler) {
26367
- void scheduler.triggerParentReopenCascade(result.reopenCascade.feedbackCard, result.reopenCascade.boardCards);
26368
- }
26369
- }
26370
26319
  return c.json({ ok: result.ok });
26371
26320
  }
26372
26321
  ).post("/start-agent", zv("json", z5.object({ workspaceId: z5.string(), cardId: z5.string() })), async (c) => {
@@ -26383,12 +26332,6 @@ var cardsController = new Hono2().post("/", zv("json", runtimeCardCreateRequestS
26383
26332
  ctx.getScheduler(workspaceId)?.stopTask(cardId);
26384
26333
  ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
26385
26334
  return c.json({ ok: true });
26386
- }).post("/interrupt-task", zv("json", z5.object({ workspaceId: z5.string(), cardId: z5.string() })), async (c) => {
26387
- const ctx = c.var.ctx;
26388
- const { workspaceId, cardId } = c.req.valid("json");
26389
- ctx.getScheduler(workspaceId)?.interruptForParentReopen(cardId);
26390
- ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
26391
- return c.json({ ok: true });
26392
26335
  }).post(
26393
26336
  "/set-pr-meta",
26394
26337
  zv(
@@ -28618,7 +28561,7 @@ process.on("uncaughtException", (err) => {
28618
28561
  if (err.code === "EPIPE" || err.code === "ECONNRESET") return;
28619
28562
  throw err;
28620
28563
  });
28621
- var VERSION9 = true ? "0.3.0" : "0.0.0-dev";
28564
+ var VERSION9 = true ? "0.5.0" : "0.0.0-dev";
28622
28565
  async function isPortAvailable(port, host) {
28623
28566
  return new Promise((resolve5) => {
28624
28567
  const probe = createServer2();