@questionbase/deskfree 0.3.0-alpha.21 → 0.3.0-alpha.23

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/index.js CHANGED
@@ -3857,19 +3857,19 @@ var DeskFreeClient = class {
3857
3857
  return this.request("POST", "messages.update", input);
3858
3858
  }
3859
3859
  /**
3860
- * Send a text message (with optional attachments or suggestions) to a DeskFree conversation.
3860
+ * Send a streaming chunk (delta only) for an in-progress message.
3861
+ * Broadcasts delta over WS and stores full content in DynamoDB for crash recovery.
3862
+ */
3863
+ async streamChunk(input) {
3864
+ return this.request("POST", "messages.streamChunk", input);
3865
+ }
3866
+ /**
3867
+ * Send a text message to a DeskFree conversation.
3861
3868
  *
3862
- * @param input - Message content, optional userId, taskId, attachments, and suggestions
3869
+ * @param input - Message content, optional userId, taskId, attachments
3863
3870
  */
3864
3871
  async sendMessage(input) {
3865
- if (!input.content && !input.suggestions) {
3866
- throw new DeskFreeError(
3867
- "client",
3868
- "content",
3869
- "content or suggestions is required",
3870
- "Missing required parameter: provide content or suggestions."
3871
- );
3872
- }
3872
+ this.requireNonEmpty(input.content, "content");
3873
3873
  return this.request("POST", "messages.send", input);
3874
3874
  }
3875
3875
  /** Fetch paginated message history for a conversation. */
@@ -3894,11 +3894,25 @@ var DeskFreeClient = class {
3894
3894
  this.requireNonEmpty(input.taskId, "taskId");
3895
3895
  return this.request("POST", "tasks.claim", input);
3896
3896
  }
3897
- /** Update the deliverable (markdown or HTML content) for a task. */
3898
- async updateDeliverable(input) {
3897
+ /** Fetch a lightweight task summary by ID. Read-only, no side effects. */
3898
+ async getTask(input) {
3899
3899
  this.requireNonEmpty(input.taskId, "taskId");
3900
- this.requireNonEmpty(input.deliverable, "deliverable");
3901
- return this.request("POST", "tasks.updateDeliverable", input);
3900
+ return this.request("GET", "tasks.get", input);
3901
+ }
3902
+ /** Update the content of an existing file. */
3903
+ async updateFile(input) {
3904
+ this.requireNonEmpty(input.fileId, "fileId");
3905
+ this.requireNonEmpty(input.content, "content");
3906
+ return this.request("POST", "files.update", input);
3907
+ }
3908
+ /** Create a new persistent file. */
3909
+ async createFile(input) {
3910
+ this.requireNonEmpty(input.name, "name");
3911
+ return this.request("POST", "files.create", input);
3912
+ }
3913
+ /** List all files for this bot (metadata only, no content). */
3914
+ async listFiles() {
3915
+ return this.request("GET", "files.list", {});
3902
3916
  }
3903
3917
  /** Send an agent status update to DeskFree. */
3904
3918
  async statusUpdate(input) {
@@ -3923,32 +3937,22 @@ var DeskFreeClient = class {
3923
3937
  this.requireNonEmpty(input.taskId, "taskId");
3924
3938
  return this.request("POST", "tasks.complete", input);
3925
3939
  }
3926
- /** Suggest new tasks for the human to review and approve (via messages.send with suggestions). */
3927
- async suggestTasks(input) {
3940
+ /** Reopen a completed/human task back to bot status for further work. */
3941
+ async reopenTask(input) {
3942
+ this.requireNonEmpty(input.taskId, "taskId");
3943
+ return this.request("POST", "tasks.reopen", input);
3944
+ }
3945
+ /** Propose a plan — creates a proposal message with plan metadata. No DB rows until human approves. */
3946
+ async proposePlan(input) {
3928
3947
  if (!input.tasks || input.tasks.length === 0) {
3929
3948
  throw new DeskFreeError(
3930
3949
  "client",
3931
3950
  "tasks",
3932
3951
  "tasks array is required and cannot be empty",
3933
- "Missing required parameter: tasks. Please provide at least one task to suggest."
3952
+ "Missing required parameter: tasks."
3934
3953
  );
3935
3954
  }
3936
- return this.sendMessage({
3937
- suggestions: input.tasks,
3938
- taskId: input.taskId
3939
- });
3940
- }
3941
- /** Suggest tasks via the dedicated bot/tasks.suggest endpoint. */
3942
- async suggestTasksDedicated(input) {
3943
- if (!input.suggestions || input.suggestions.length === 0) {
3944
- throw new DeskFreeError(
3945
- "client",
3946
- "suggestions",
3947
- "suggestions array is required and cannot be empty",
3948
- "Missing required parameter: suggestions."
3949
- );
3950
- }
3951
- return this.request("POST", "tasks.suggest", input);
3955
+ return this.request("POST", "tasks.propose", input);
3952
3956
  }
3953
3957
  /**
3954
3958
  * Claim a pending evaluation for a task. Atomically sets isWorking=true where
@@ -4121,6 +4125,8 @@ function resolvePluginStorePath(subpath) {
4121
4125
 
4122
4126
  // src/streaming.ts
4123
4127
  var THROTTLE_MS = 300;
4128
+ var CHAR_BUFFER_SIZE = 256;
4129
+ var CLOSE_MAX_RETRIES = 3;
4124
4130
  var DeskFreeStreamingSession = class {
4125
4131
  client;
4126
4132
  log;
@@ -4128,9 +4134,11 @@ var DeskFreeStreamingSession = class {
4128
4134
  currentText = "";
4129
4135
  closed = false;
4130
4136
  lastUpdateTime = 0;
4131
- pendingText = null;
4137
+ pendingDelta = "";
4132
4138
  pendingTimer = null;
4133
4139
  queue = Promise.resolve();
4140
+ /** Full accumulated text (currentText + pendingDelta) */
4141
+ fullText = "";
4134
4142
  constructor(client, log) {
4135
4143
  this.client = client;
4136
4144
  this.log = log;
@@ -4146,51 +4154,72 @@ var DeskFreeStreamingSession = class {
4146
4154
  const result = await this.client.sendMessage({ content, taskId });
4147
4155
  this.messageId = result.messageId;
4148
4156
  this.currentText = content;
4157
+ this.fullText = content;
4149
4158
  this.lastUpdateTime = Date.now();
4150
4159
  this.log.info(`Streaming started: messageId=${this.messageId}`);
4151
4160
  return this.messageId;
4152
4161
  }
4153
4162
  /**
4154
- * Update the message with new content. Throttled to THROTTLE_MS.
4155
- * Content should be the FULL message text (not a delta).
4163
+ * Push new text (appended to the stream). Buffers and flushes as delta.
4164
+ * Content should be the FULL accumulated message text.
4156
4165
  */
4157
4166
  async update(fullText) {
4158
4167
  if (!this.messageId || this.closed) return;
4159
- if (fullText === this.currentText) return;
4168
+ if (fullText === this.fullText) return;
4169
+ const delta = fullText.slice(this.fullText.length);
4170
+ if (!delta) return;
4171
+ this.fullText = fullText;
4172
+ this.pendingDelta += delta;
4160
4173
  const now = Date.now();
4161
4174
  const elapsed = now - this.lastUpdateTime;
4162
- if (elapsed >= THROTTLE_MS) {
4163
- await this.sendUpdate(fullText, true);
4164
- } else {
4165
- this.pendingText = fullText;
4166
- if (!this.pendingTimer) {
4167
- this.pendingTimer = setTimeout(async () => {
4168
- this.pendingTimer = null;
4169
- if (this.pendingText && !this.closed) {
4170
- const text = this.pendingText;
4171
- this.pendingText = null;
4172
- await this.sendUpdate(text, true);
4173
- }
4174
- }, THROTTLE_MS - elapsed);
4175
- }
4175
+ const bufferFull = this.pendingDelta.length >= CHAR_BUFFER_SIZE;
4176
+ const throttleExpired = elapsed >= THROTTLE_MS;
4177
+ if (bufferFull || throttleExpired) {
4178
+ this.flush();
4179
+ } else if (!this.pendingTimer) {
4180
+ this.pendingTimer = setTimeout(() => {
4181
+ this.pendingTimer = null;
4182
+ this.flush();
4183
+ }, THROTTLE_MS - elapsed);
4176
4184
  }
4177
4185
  }
4178
4186
  /**
4179
4187
  * Finalize the streaming session with the final content.
4188
+ * Retries up to 3 times with exponential backoff.
4180
4189
  */
4181
4190
  async close(finalText) {
4182
4191
  if (this.closed) return;
4183
- this.closed = true;
4184
4192
  if (this.pendingTimer) {
4185
4193
  clearTimeout(this.pendingTimer);
4186
4194
  this.pendingTimer = null;
4187
4195
  }
4196
+ this.flush();
4197
+ this.closed = true;
4188
4198
  await this.queue;
4189
- const text = finalText ?? this.pendingText ?? this.currentText;
4190
- if (this.messageId && text !== this.currentText) {
4191
- await this.sendUpdate(text, false);
4192
- } else if (this.messageId) {
4193
- await this.sendUpdate(this.currentText, false);
4199
+ const text = finalText ?? (this.fullText || this.currentText);
4200
+ if (this.messageId) {
4201
+ for (let attempt = 0; attempt < CLOSE_MAX_RETRIES; attempt++) {
4202
+ try {
4203
+ await this.client.updateMessage({
4204
+ messageId: this.messageId,
4205
+ content: text,
4206
+ streaming: false
4207
+ });
4208
+ break;
4209
+ } catch (err) {
4210
+ const msg = err instanceof Error ? err.message : String(err);
4211
+ if (attempt < CLOSE_MAX_RETRIES - 1) {
4212
+ this.log.warn(
4213
+ `Streaming close attempt ${attempt + 1} failed: ${msg}, retrying...`
4214
+ );
4215
+ await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
4216
+ } else {
4217
+ this.log.error(
4218
+ `Streaming close failed after ${CLOSE_MAX_RETRIES} attempts: ${msg}. Recovery worker will finalize.`
4219
+ );
4220
+ }
4221
+ }
4222
+ }
4194
4223
  }
4195
4224
  this.log.info(`Streaming closed: messageId=${this.messageId}`);
4196
4225
  }
@@ -4200,23 +4229,32 @@ var DeskFreeStreamingSession = class {
4200
4229
  getMessageId() {
4201
4230
  return this.messageId;
4202
4231
  }
4203
- async sendUpdate(content, streaming) {
4204
- if (!this.messageId) return;
4232
+ /**
4233
+ * Flush the pending delta buffer via streamChunk.
4234
+ */
4235
+ flush() {
4236
+ if (this.pendingTimer) {
4237
+ clearTimeout(this.pendingTimer);
4238
+ this.pendingTimer = null;
4239
+ }
4240
+ const delta = this.pendingDelta;
4241
+ if (!delta || !this.messageId || this.closed) return;
4242
+ this.pendingDelta = "";
4243
+ const fullContent = this.fullText;
4205
4244
  this.queue = this.queue.then(async () => {
4206
4245
  try {
4207
- await this.client.updateMessage({
4246
+ await this.client.streamChunk({
4208
4247
  messageId: this.messageId,
4209
- content,
4210
- streaming
4248
+ delta,
4249
+ fullContent
4211
4250
  });
4212
- this.currentText = content;
4251
+ this.currentText = fullContent;
4213
4252
  this.lastUpdateTime = Date.now();
4214
4253
  } catch (err) {
4215
4254
  const msg = err instanceof Error ? err.message : String(err);
4216
- this.log.warn(`Streaming update failed: ${msg}`);
4255
+ this.log.warn(`Streaming delta failed: ${msg}`);
4217
4256
  }
4218
4257
  });
4219
- await this.queue;
4220
4258
  }
4221
4259
  };
4222
4260
 
@@ -4429,6 +4467,38 @@ async function fetchAndSaveMedia(attachment) {
4429
4467
  }
4430
4468
  }
4431
4469
  }
4470
+ var MAX_INSTRUCTIONS_LENGTH = 500;
4471
+ var MAX_CONTEXT_MESSAGES = 5;
4472
+ function truncateAtWord(text, maxLen) {
4473
+ if (text.length <= maxLen) return text;
4474
+ const truncated = text.slice(0, maxLen);
4475
+ const lastSpace = truncated.lastIndexOf(" ");
4476
+ return (lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated) + "\u2026";
4477
+ }
4478
+ function buildBodyForAgent(task, content, recentMessages) {
4479
+ let prefix = `[Task thread: "${task.title}" | status: ${task.status} | isWorking: ${task.isWorking}]`;
4480
+ if (task.instructions) {
4481
+ prefix += "\n" + truncateAtWord(task.instructions, MAX_INSTRUCTIONS_LENGTH);
4482
+ }
4483
+ if (recentMessages && recentMessages.length > 0) {
4484
+ prefix += "\n\nRecent thread messages:";
4485
+ for (const msg of recentMessages) {
4486
+ const author = msg.authorType === "bot" ? "\u{1F916} Bot" : msg.userName ?? "Human";
4487
+ prefix += `
4488
+ - ${author}: ${truncateAtWord(msg.content, 200)}`;
4489
+ }
4490
+ }
4491
+ return prefix + "\n\n" + content;
4492
+ }
4493
+ function resolveTaskRouting(task, hasAttachments) {
4494
+ if (task.isWorking) {
4495
+ return { target: "runner" };
4496
+ }
4497
+ if (hasAttachments && (task.status === "human" || task.status === "done")) {
4498
+ return { target: "auto-reopen" };
4499
+ }
4500
+ return { target: "orchestrator" };
4501
+ }
4432
4502
  async function deliverMessageToAgent(ctx, message, client) {
4433
4503
  const runtime = getDeskFreeRuntime();
4434
4504
  const log = ctx.log ?? runtime.logging.createLogger("deskfree:deliver");
@@ -4471,9 +4541,55 @@ async function deliverMessageToAgent(ctx, message, client) {
4471
4541
  );
4472
4542
  }
4473
4543
  }
4544
+ let bodyForAgent;
4545
+ let routingTarget = "orchestrator";
4546
+ if (message.taskId) {
4547
+ try {
4548
+ const task = await client.getTask({ taskId: message.taskId });
4549
+ const routing = resolveTaskRouting(task, mediaPaths.length > 0);
4550
+ routingTarget = routing.target;
4551
+ if (routingTarget === "auto-reopen") {
4552
+ log.info(
4553
+ `Auto-reopening task ${message.taskId} (attachment on ${task.status} task)`
4554
+ );
4555
+ try {
4556
+ await client.reopenTask({
4557
+ taskId: message.taskId,
4558
+ reason: "Human sent an attachment \u2014 reopening for further work"
4559
+ });
4560
+ } catch (reopenErr) {
4561
+ const reopenMsg = reopenErr instanceof Error ? reopenErr.message : String(reopenErr);
4562
+ log.warn(
4563
+ `Failed to auto-reopen task ${message.taskId}: ${reopenMsg}`
4564
+ );
4565
+ routingTarget = "orchestrator";
4566
+ }
4567
+ }
4568
+ let recentMessages;
4569
+ try {
4570
+ const msgList = await client.listMessages({
4571
+ taskId: message.taskId,
4572
+ limit: MAX_CONTEXT_MESSAGES
4573
+ });
4574
+ recentMessages = msgList.items.map((m) => ({
4575
+ authorType: m.humanId ? "user" : "bot",
4576
+ content: m.content,
4577
+ userName: m.userName
4578
+ }));
4579
+ } catch {
4580
+ }
4581
+ bodyForAgent = buildBodyForAgent(task, message.content, recentMessages);
4582
+ } catch (err) {
4583
+ const errMsg = err instanceof Error ? err.message : String(err);
4584
+ log.warn(`Failed to fetch task context for ${message.taskId}: ${errMsg}`);
4585
+ }
4586
+ }
4587
+ const sessionKey = routingTarget === "runner" && message.taskId ? `agent:main:deskfree:task:${message.taskId}` : `agent:main:deskfree:dm:${peerId}`;
4588
+ log.info(`Routing message to ${routingTarget} (session: ${sessionKey})`);
4474
4589
  const msgCtx = runtime.channel.reply.finalizeInboundContext({
4475
4590
  Body: message.content,
4476
4591
  RawBody: message.content,
4592
+ ...bodyForAgent ? { BodyForAgent: bodyForAgent } : {},
4477
4593
  ChatType: "dm",
4478
4594
  Provider: "deskfree",
4479
4595
  Surface: "deskfree",
@@ -4485,7 +4601,7 @@ async function deliverMessageToAgent(ctx, message, client) {
4485
4601
  Timestamp: message.createdAt,
4486
4602
  AccountId: ctx.accountId,
4487
4603
  FromName: message.userName ?? "Unknown",
4488
- SessionKey: `agent:main:deskfree:dm:${peerId}`,
4604
+ SessionKey: sessionKey,
4489
4605
  // Backward-compatible: first attachment
4490
4606
  MediaPath: mediaPaths[0],
4491
4607
  MediaType: mediaTypes[0],
@@ -4496,10 +4612,10 @@ async function deliverMessageToAgent(ctx, message, client) {
4496
4612
  MediaUrls: mediaUrls,
4497
4613
  MediaCount: mediaPaths.length
4498
4614
  });
4615
+ let streamingSession = null;
4616
+ let accumulatedText = "";
4499
4617
  try {
4500
4618
  const cfg = runtime.config.loadConfig();
4501
- let streamingSession = null;
4502
- let accumulatedText = "";
4503
4619
  const { dispatcher, replyOptions } = runtime.channel.reply.createReplyDispatcherWithTyping({
4504
4620
  channel: "deskfree",
4505
4621
  accountId: ctx.accountId,
@@ -4553,6 +4669,14 @@ async function deliverMessageToAgent(ctx, message, client) {
4553
4669
  });
4554
4670
  log.info(`Message ${message.messageId} dispatched successfully.`);
4555
4671
  } catch (err) {
4672
+ const session = streamingSession;
4673
+ if (session?.isActive()) {
4674
+ try {
4675
+ await session.close(accumulatedText || void 0);
4676
+ } catch {
4677
+ }
4678
+ streamingSession = null;
4679
+ }
4556
4680
  const errMsg = err instanceof Error ? err.message : String(err);
4557
4681
  log.warn(`Failed to dispatch message ${message.messageId}: ${errMsg}`);
4558
4682
  reportError("error", `Failed to dispatch message: ${errMsg}`, {
@@ -4585,22 +4709,21 @@ var wrapper_default = import_websocket.default;
4585
4709
  // src/gateway.ts
4586
4710
  var activeTaskId = null;
4587
4711
  var completedTaskId = null;
4588
- function setActiveTaskId(taskId) {
4589
- if (taskId === null && activeTaskId !== null) {
4590
- completedTaskId = activeTaskId;
4591
- }
4592
- activeTaskId = taskId;
4593
- }
4712
+ var inboundThreadId = null;
4594
4713
  function getActiveTaskId() {
4595
- return activeTaskId ?? completedTaskId;
4714
+ return activeTaskId ?? completedTaskId ?? inboundThreadId;
4596
4715
  }
4597
4716
  function clearCompletedTaskId() {
4598
4717
  completedTaskId = null;
4599
4718
  }
4719
+ function setInboundThreadId(taskId) {
4720
+ inboundThreadId = taskId;
4721
+ }
4600
4722
  var PING_INTERVAL_MS = 5 * 60 * 1e3;
4601
4723
  var POLL_FALLBACK_INTERVAL_MS = 30 * 1e3;
4602
4724
  var WS_CONNECTION_TIMEOUT_MS = 30 * 1e3;
4603
4725
  var WS_PONG_TIMEOUT_MS = 10 * 1e3;
4726
+ var NOTIFY_DEBOUNCE_MS = 200;
4604
4727
  var BACKOFF_INITIAL_MS = 2e3;
4605
4728
  var BACKOFF_MAX_MS = 3e4;
4606
4729
  var BACKOFF_FACTOR = 1.8;
@@ -4824,6 +4947,7 @@ async function runWebSocketConnection(opts) {
4824
4947
  let pingInterval;
4825
4948
  let connectionTimer;
4826
4949
  let pongTimer;
4950
+ let notifyDebounceTimer;
4827
4951
  let isConnected = false;
4828
4952
  const cleanup = () => {
4829
4953
  if (pingInterval !== void 0) {
@@ -4838,6 +4962,10 @@ async function runWebSocketConnection(opts) {
4838
4962
  clearTimeout(pongTimer);
4839
4963
  pongTimer = void 0;
4840
4964
  }
4965
+ if (notifyDebounceTimer !== void 0) {
4966
+ clearTimeout(notifyDebounceTimer);
4967
+ notifyDebounceTimer = void 0;
4968
+ }
4841
4969
  };
4842
4970
  connectionTimer = setTimeout(() => {
4843
4971
  if (!isConnected) {
@@ -4918,15 +5046,21 @@ async function runWebSocketConnection(opts) {
4918
5046
  return;
4919
5047
  }
4920
5048
  if (msg.action === "notify") {
4921
- enqueuePoll(
4922
- client,
4923
- ctx,
4924
- () => cursor,
4925
- (c) => {
4926
- cursor = c ?? cursor;
4927
- },
4928
- log
4929
- );
5049
+ if (notifyDebounceTimer !== void 0) {
5050
+ clearTimeout(notifyDebounceTimer);
5051
+ }
5052
+ notifyDebounceTimer = setTimeout(() => {
5053
+ notifyDebounceTimer = void 0;
5054
+ enqueuePoll(
5055
+ client,
5056
+ ctx,
5057
+ () => cursor,
5058
+ (c) => {
5059
+ cursor = c ?? cursor;
5060
+ },
5061
+ log
5062
+ );
5063
+ }, NOTIFY_DEBOUNCE_MS);
4930
5064
  } else if (msg.action === "pong") {
4931
5065
  if (pongTimer !== void 0) {
4932
5066
  clearTimeout(pongTimer);
@@ -5123,6 +5257,7 @@ async function pollAndDeliver(client, ctx, cursor, log, account) {
5123
5257
  continue;
5124
5258
  }
5125
5259
  clearCompletedTaskId();
5260
+ setInboundThreadId(message.taskId ?? null);
5126
5261
  await deliverMessageToAgent(ctx, message, client);
5127
5262
  deliveredMessageIds.add(message.messageId);
5128
5263
  deliveredCount++;
@@ -7765,76 +7900,67 @@ var ORCHESTRATOR_TOOLS = {
7765
7900
  description: "Get full workspace state \u2014 all tasks, recently done tasks, active initiatives. Use to assess what needs attention.",
7766
7901
  parameters: Type.Object({})
7767
7902
  },
7768
- START_TASK: {
7769
- name: "deskfree_start_task",
7770
- description: "Claim a bot task (isWorking=false) and start working. Returns full context (instructions, deliverable, message history).",
7903
+ REOPEN_TASK: {
7904
+ name: "deskfree_reopen_task",
7905
+ description: "Reopen a completed or human-side task back to bot status. Use when a human message in a task thread indicates more work is needed. The task becomes available for a worker to claim.",
7771
7906
  parameters: Type.Object({
7772
- taskId: Type.String({ description: "Task UUID to claim" })
7773
- })
7774
- },
7775
- UPDATE_DELIVERABLE: {
7776
- name: "deskfree_update_deliverable",
7777
- description: 'Update task deliverable. Build incrementally as you work. Use format="html" when delivering rich web content (reports, dashboards, interactive pages); use format="markdown" (default) for everything else.',
7778
- parameters: Type.Object({
7779
- taskId: Type.String({ description: "Task UUID" }),
7780
- deliverable: Type.String({
7781
- description: "Deliverable content (markdown or HTML depending on format)"
7782
- }),
7783
- format: Type.Optional(
7784
- Type.Union([Type.Literal("markdown"), Type.Literal("html")], {
7785
- description: '"markdown" (default) for text/documents, "html" for rich web content. HTML is rendered in a sandboxed iframe.'
7907
+ taskId: Type.String({ description: "Task UUID to reopen" }),
7908
+ reason: Type.Optional(
7909
+ Type.String({
7910
+ description: "Why this task is being reopened (shown in task thread as system message)"
7786
7911
  })
7787
7912
  )
7788
7913
  })
7789
7914
  },
7790
- COMPLETE_TASK: {
7791
- name: "deskfree_complete_task",
7792
- description: 'Finish a task. Outcome "done" = work complete for review. Outcome "blocked" = need human input. Both move to human.',
7793
- parameters: Type.Object({
7794
- taskId: Type.String({ description: "Task UUID" }),
7795
- outcome: Type.Union([Type.Literal("done"), Type.Literal("blocked")], {
7796
- description: '"done" = work complete, "blocked" = need human input'
7797
- })
7798
- })
7799
- },
7800
7915
  SEND_MESSAGE: {
7801
7916
  name: "deskfree_send_message",
7802
- description: "Send a message in the task thread (progress update, question, status report). Can also suggest follow-up tasks for human review by providing the suggestions parameter instead of content.",
7917
+ description: "Send a message (progress update, question, status report). Used for communication outside of task threads.",
7803
7918
  parameters: Type.Object({
7804
- content: Type.Optional(
7805
- Type.String({
7806
- description: "Message content. Required unless suggestions is provided."
7807
- })
7808
- ),
7919
+ content: Type.String({
7920
+ description: "Message content."
7921
+ }),
7809
7922
  taskId: Type.Optional(
7810
7923
  Type.String({
7811
- description: "Task UUID (optional if context provides it)"
7924
+ description: "Task UUID (optional \u2014 threads into task if provided)"
7812
7925
  })
7813
- ),
7814
- suggestions: Type.Optional(
7815
- Type.Array(
7816
- Type.Object({
7817
- title: Type.String({ description: "Task title (max 200 chars)" }),
7818
- instructions: Type.Optional(
7819
- Type.String({
7820
- description: "Detailed instructions for the suggested task"
7821
- })
7822
- )
7823
- }),
7824
- {
7825
- description: "Suggest tasks for human review (1-10). The human will see approve/reject buttons for each. Provide this instead of content.",
7826
- minItems: 1,
7827
- maxItems: 10
7828
- }
7829
- )
7830
7926
  )
7831
7927
  })
7832
7928
  },
7833
- SUGGEST_TASKS: {
7834
- name: "deskfree_suggest_tasks",
7835
- description: 'Suggest tasks for human approval. Tasks are created with "suggested" status \u2014 the human approves or rejects each one. Use this to propose work before starting. Include detailed instructions as if briefing a contractor.',
7929
+ PROPOSE: {
7930
+ name: "deskfree_propose",
7931
+ description: "Propose a plan for human approval. Nothing is created until the human reviews and approves in a modal. One initiative per call \u2014 make multiple calls for multiple initiatives. Include substeps as human-reviewable checklist items.",
7836
7932
  parameters: Type.Object({
7837
- suggestions: Type.Array(
7933
+ initiative: Type.Optional(
7934
+ Type.Union(
7935
+ [
7936
+ Type.String({
7937
+ description: 'Existing initiative ID (e.g. "INI_123") to group tasks under'
7938
+ }),
7939
+ Type.Object(
7940
+ {
7941
+ title: Type.String({
7942
+ description: "New initiative title (max 200 chars)"
7943
+ }),
7944
+ content: Type.String({
7945
+ description: "Initiative content markdown \u2014 current state, approach, and next priorities"
7946
+ })
7947
+ },
7948
+ {
7949
+ description: "Create a new initiative \u2014 human approves it along with tasks"
7950
+ }
7951
+ )
7952
+ ],
7953
+ {
7954
+ description: "Initiative to group tasks under. Pass an existing ID string, a {title, content} object to create new, or omit for general/no initiative."
7955
+ }
7956
+ )
7957
+ ),
7958
+ context: Type.Optional(
7959
+ Type.String({
7960
+ description: "Why this plan is being proposed \u2014 helps the human understand the reasoning behind the proposal"
7961
+ })
7962
+ ),
7963
+ tasks: Type.Array(
7838
7964
  Type.Object({
7839
7965
  title: Type.String({
7840
7966
  description: "Task title \u2014 short, action-oriented (max 200 chars)"
@@ -7844,110 +7970,278 @@ var ORCHESTRATOR_TOOLS = {
7844
7970
  description: "Detailed instructions: what to do, why, what done looks like, known constraints"
7845
7971
  })
7846
7972
  ),
7973
+ substeps: Type.Optional(
7974
+ Type.Array(Type.String(), {
7975
+ description: "Human-reviewable checklist items. The human can toggle each on/off before approving. Use for discrete steps within the task.",
7976
+ minItems: 1,
7977
+ maxItems: 20
7978
+ })
7979
+ ),
7980
+ file: Type.Optional(
7981
+ Type.Union(
7982
+ [
7983
+ Type.Object(
7984
+ {
7985
+ existingId: Type.String({
7986
+ description: "Existing file ID to link to this task"
7987
+ })
7988
+ },
7989
+ {
7990
+ description: "Link an existing file \u2014 bot receives its content when claiming the task"
7991
+ }
7992
+ ),
7993
+ Type.Object(
7994
+ {
7995
+ name: Type.String({
7996
+ description: "File name (max 200 chars)"
7997
+ }),
7998
+ description: Type.Optional(
7999
+ Type.String({
8000
+ description: "Brief description of the file's purpose"
8001
+ })
8002
+ )
8003
+ },
8004
+ {
8005
+ description: "Create a new file on approval \u2014 use when the task will produce a persistent document"
8006
+ }
8007
+ )
8008
+ ],
8009
+ {
8010
+ description: "File to link: { existingId } for existing files, or { name, description? } to create a new one on approval. Omit if no file needed."
8011
+ }
8012
+ )
8013
+ ),
7847
8014
  estimatedTokens: Type.Optional(
7848
8015
  Type.Number({
7849
8016
  description: "Estimated token cost \u2014 consider files to read, reasoning, output"
7850
8017
  })
7851
8018
  ),
7852
- dependsOn: Type.Optional(
7853
- Type.Array(Type.String(), {
7854
- description: "Task IDs this suggestion depends on (blocks claiming until those are done)"
7855
- })
7856
- ),
7857
- initiativeId: Type.Optional(
8019
+ scheduledFor: Type.Optional(
7858
8020
  Type.String({
7859
- description: "Link to existing initiative ID \u2014 set when this task belongs to an active initiative"
8021
+ description: "ISO-8601 date for when this task should become available. Use for future-dated or recurring work."
7860
8022
  })
7861
8023
  )
7862
8024
  }),
7863
8025
  {
7864
- description: "Array of task suggestions (1-20)",
8026
+ description: "Array of tasks to propose (1-20)",
7865
8027
  minItems: 1,
7866
8028
  maxItems: 20
7867
8029
  }
7868
- ),
7869
- parentTaskId: Type.Optional(
8030
+ )
8031
+ })
8032
+ }
8033
+ };
8034
+ var SHARED_TOOLS = {
8035
+ UPDATE_FILE: {
8036
+ name: "deskfree_update_file",
8037
+ description: `Update a file's content. Use this to save work to a persistent file linked to your task. Call incrementally as you build content. Use format="html" for rich web content; use format="markdown" (default) for everything else.`,
8038
+ parameters: Type.Object({
8039
+ fileId: Type.String({ description: "File ID to update" }),
8040
+ content: Type.String({
8041
+ description: "Full file content (replaces previous)"
8042
+ }),
8043
+ contentFormat: Type.Optional(
8044
+ Type.Union([Type.Literal("markdown"), Type.Literal("html")], {
8045
+ description: '"markdown" (default) for text/documents, "html" for rich web content.'
8046
+ })
8047
+ )
8048
+ })
8049
+ },
8050
+ COMPLETE_TASK: {
8051
+ name: "deskfree_complete_task",
8052
+ description: 'Finish a task. Outcome "done" = work complete (summary required). Outcome "blocked" = need human input. Both move to human. For evaluation tasks (mode="evaluation"), include the evaluation object when outcome is "done".',
8053
+ parameters: Type.Object({
8054
+ taskId: Type.String({ description: "Task UUID" }),
8055
+ outcome: Type.Union([Type.Literal("done"), Type.Literal("blocked")], {
8056
+ description: '"done" = work complete, "blocked" = need human input'
8057
+ }),
8058
+ summary: Type.Optional(
7870
8059
  Type.String({
7871
- description: "Parent task ID \u2014 set when suggesting follow-ups from within a task"
8060
+ description: 'Brief summary of what was accomplished (required for outcome "done", max 2000 chars)'
7872
8061
  })
7873
8062
  ),
7874
- initiativeSuggestions: Type.Optional(
7875
- Type.Array(
7876
- Type.Object({
7877
- title: Type.String({
7878
- description: "Initiative title (max 200 chars)"
7879
- }),
7880
- content: Type.String({
7881
- description: "Initiative content markdown \u2014 current state, approach, and next priorities"
8063
+ evaluation: Type.Optional(
8064
+ Type.Object(
8065
+ {
8066
+ reasoning: Type.String({
8067
+ description: "Explanation of your evaluation \u2014 what you analyzed and why you did or did not update the ways of working and/or initiative content"
7882
8068
  }),
7883
- taskRefs: Type.Optional(
7884
- Type.Array(Type.Number(), {
7885
- description: "Indexes into suggestions[] to auto-link when this initiative is approved"
7886
- })
8069
+ globalWoW: Type.Object(
8070
+ {
8071
+ hasChanges: Type.Boolean({
8072
+ description: "Whether the global ways of working should be updated"
8073
+ }),
8074
+ updatedContent: Type.Optional(
8075
+ Type.String({
8076
+ description: "Full updated global ways-of-working markdown (required if hasChanges=true)"
8077
+ })
8078
+ )
8079
+ },
8080
+ {
8081
+ description: "Global ways-of-working update \u2014 patterns that apply across all work"
8082
+ }
8083
+ ),
8084
+ initiative: Type.Optional(
8085
+ Type.Object(
8086
+ {
8087
+ hasChanges: Type.Boolean({
8088
+ description: "Whether the initiative content should be updated (ignored if task has no initiative)"
8089
+ }),
8090
+ updatedContent: Type.Optional(
8091
+ Type.String({
8092
+ description: "Full updated initiative content markdown (required if hasChanges=true)"
8093
+ })
8094
+ )
8095
+ },
8096
+ {
8097
+ description: "Initiative content update \u2014 what was learned about this specific area of focus. Ignored if the task has no initiative_id."
8098
+ }
8099
+ )
7887
8100
  )
7888
- }),
8101
+ },
7889
8102
  {
7890
- description: 'Propose new initiatives (optional). Created with "suggested" status \u2014 human approves or rejects independently.',
7891
- minItems: 1,
7892
- maxItems: 10
8103
+ description: 'Required when completing an evaluation task (mode="evaluation") with outcome "done". Review global WoW and initiative content and provide your analysis.'
7893
8104
  }
7894
8105
  )
7895
8106
  )
7896
8107
  })
7897
8108
  },
7898
- CLAIM_EVALUATION: {
7899
- name: "deskfree_claim_evaluation",
7900
- description: "Claim a pending ways-of-working evaluation for a task. Returns the task, its message history, current global ways_of_working, and initiative data (if the task belongs to an initiative). Returns null if already claimed by another process.",
8109
+ SEND_MESSAGE: {
8110
+ name: "deskfree_send_message",
8111
+ description: "Send a message in the task thread (progress update, question, status report).",
7901
8112
  parameters: Type.Object({
7902
- taskId: Type.String({ description: "Task UUID to claim evaluation for" })
8113
+ content: Type.String({
8114
+ description: "Message content."
8115
+ }),
8116
+ taskId: Type.Optional(
8117
+ Type.String({
8118
+ description: "Task UUID (optional if context provides it)"
8119
+ })
8120
+ )
7903
8121
  })
7904
8122
  },
7905
- SUBMIT_EVALUATION: {
7906
- name: "deskfree_submit_evaluation",
7907
- description: "Submit the result of a ways-of-working evaluation. Provide reasoning explaining your analysis. Evaluation has two independent outputs: globalWoW (applies everywhere) and initiative (applies to the specific initiative this task belongs to). Set hasChanges=true and provide updatedContent for each output you want to update.",
8123
+ PROPOSE: {
8124
+ name: "deskfree_propose",
8125
+ description: "Propose a plan for human approval. Nothing is created until the human reviews and approves in a modal. One initiative per call \u2014 make multiple calls for multiple initiatives. Include substeps as human-reviewable checklist items.",
7908
8126
  parameters: Type.Object({
7909
- taskId: Type.String({ description: "Task UUID being evaluated" }),
7910
- reasoning: Type.String({
7911
- description: "Explanation of your evaluation \u2014 what you analyzed and why you did or did not update the ways of working and/or initiative content"
7912
- }),
7913
- globalWoW: Type.Object(
7914
- {
7915
- hasChanges: Type.Boolean({
7916
- description: "Whether the global ways of working should be updated"
7917
- }),
7918
- updatedContent: Type.Optional(
8127
+ initiative: Type.Optional(
8128
+ Type.Union(
8129
+ [
7919
8130
  Type.String({
7920
- description: "Full updated global ways-of-working markdown (required if hasChanges=true)"
7921
- })
7922
- )
7923
- },
7924
- {
7925
- description: "Global ways-of-working update \u2014 patterns that apply across all work"
7926
- }
8131
+ description: 'Existing initiative ID (e.g. "INI_123") to group tasks under'
8132
+ }),
8133
+ Type.Object(
8134
+ {
8135
+ title: Type.String({
8136
+ description: "New initiative title (max 200 chars)"
8137
+ }),
8138
+ content: Type.String({
8139
+ description: "Initiative content markdown \u2014 current state, approach, and next priorities"
8140
+ })
8141
+ },
8142
+ {
8143
+ description: "Create a new initiative \u2014 human approves it along with tasks"
8144
+ }
8145
+ )
8146
+ ],
8147
+ {
8148
+ description: "Initiative to group tasks under. Pass an existing ID string, a {title, content} object to create new, or omit for general/no initiative."
8149
+ }
8150
+ )
7927
8151
  ),
7928
- initiative: Type.Object(
7929
- {
7930
- hasChanges: Type.Boolean({
7931
- description: "Whether the initiative content should be updated (ignored if task has no initiative)"
8152
+ context: Type.Optional(
8153
+ Type.String({
8154
+ description: "Why this plan is being proposed \u2014 helps the human understand the reasoning behind the proposal"
8155
+ })
8156
+ ),
8157
+ tasks: Type.Array(
8158
+ Type.Object({
8159
+ title: Type.String({
8160
+ description: "Task title \u2014 short, action-oriented (max 200 chars)"
7932
8161
  }),
7933
- updatedContent: Type.Optional(
8162
+ instructions: Type.Optional(
7934
8163
  Type.String({
7935
- description: "Full updated initiative content markdown (required if hasChanges=true)"
8164
+ description: "Detailed instructions: what to do, why, what done looks like, known constraints"
8165
+ })
8166
+ ),
8167
+ substeps: Type.Optional(
8168
+ Type.Array(Type.String(), {
8169
+ description: "Human-reviewable checklist items. The human can toggle each on/off before approving. Use for discrete steps within the task.",
8170
+ minItems: 1,
8171
+ maxItems: 20
8172
+ })
8173
+ ),
8174
+ file: Type.Optional(
8175
+ Type.Union(
8176
+ [
8177
+ Type.Object(
8178
+ {
8179
+ existingId: Type.String({
8180
+ description: "Existing file ID to link to this task"
8181
+ })
8182
+ },
8183
+ {
8184
+ description: "Link an existing file \u2014 bot receives its content when claiming the task"
8185
+ }
8186
+ ),
8187
+ Type.Object(
8188
+ {
8189
+ name: Type.String({
8190
+ description: "File name (max 200 chars)"
8191
+ }),
8192
+ description: Type.Optional(
8193
+ Type.String({
8194
+ description: "Brief description of the file's purpose"
8195
+ })
8196
+ )
8197
+ },
8198
+ {
8199
+ description: "Create a new file on approval \u2014 use when the task will produce a persistent document"
8200
+ }
8201
+ )
8202
+ ],
8203
+ {
8204
+ description: "File to link: { existingId } for existing files, or { name, description? } to create a new one on approval. Omit if no file needed."
8205
+ }
8206
+ )
8207
+ ),
8208
+ estimatedTokens: Type.Optional(
8209
+ Type.Number({
8210
+ description: "Estimated token cost \u2014 consider files to read, reasoning, output"
8211
+ })
8212
+ ),
8213
+ scheduledFor: Type.Optional(
8214
+ Type.String({
8215
+ description: "ISO-8601 date for when this task should become available. Use for future-dated or recurring work."
7936
8216
  })
7937
8217
  )
7938
- },
8218
+ }),
7939
8219
  {
7940
- description: "Initiative content update \u2014 what was learned about this specific area of focus. Ignored if the task has no initiative_id."
8220
+ description: "Array of tasks to propose (1-20)",
8221
+ minItems: 1,
8222
+ maxItems: 20
7941
8223
  }
7942
8224
  )
7943
8225
  })
7944
8226
  }
7945
8227
  };
7946
8228
  var WORKER_TOOLS = {
7947
- UPDATE_DELIVERABLE: ORCHESTRATOR_TOOLS.UPDATE_DELIVERABLE,
7948
- COMPLETE_TASK: ORCHESTRATOR_TOOLS.COMPLETE_TASK,
7949
- SEND_MESSAGE: ORCHESTRATOR_TOOLS.SEND_MESSAGE,
7950
- SUBMIT_EVALUATION: ORCHESTRATOR_TOOLS.SUBMIT_EVALUATION
8229
+ START_TASK: {
8230
+ name: "deskfree_start_task",
8231
+ description: 'Claim a bot task (isWorking=false) and start working. Returns full context (instructions, message history, and fileContext if the task has a linked file \u2014 use the file content as working context). For evaluation tasks, also returns WoW context and mode="evaluation".',
8232
+ parameters: Type.Object({
8233
+ taskId: Type.String({ description: "Task UUID to claim" }),
8234
+ runnerId: Type.Optional(
8235
+ Type.String({
8236
+ description: "Runner/session identifier for tracking (max 100 chars)"
8237
+ })
8238
+ )
8239
+ })
8240
+ },
8241
+ UPDATE_FILE: SHARED_TOOLS.UPDATE_FILE,
8242
+ COMPLETE_TASK: SHARED_TOOLS.COMPLETE_TASK,
8243
+ SEND_MESSAGE: SHARED_TOOLS.SEND_MESSAGE,
8244
+ PROPOSE: SHARED_TOOLS.PROPOSE
7951
8245
  };
7952
8246
  var CHANNEL_META = {
7953
8247
  name: "DeskFree",
@@ -8504,24 +8798,30 @@ var deskFreePlugin = {
8504
8798
  };
8505
8799
 
8506
8800
  // src/context.ts
8507
- var DESKFREE_AGENT_DIRECTIVE = `## DeskFree \u2014 The Work Loop
8508
- Always read the deskfree skill (SKILL.md) at startup. Follow the suggest-first work loop:
8509
- 1. **Check state** \u2192 \`deskfree_state\` \u2014 see what needs attention, read ways of working.
8510
- 2. **Suggest tasks** \u2192 \`deskfree_suggest_tasks\` \u2014 ALL work requires human-approved tasks first.
8511
- 3. **Claim a task** \u2192 \`deskfree_start_task\` \u2014 read instructions + parent context carefully.
8512
- 4. **Do the work** \u2192 \`deskfree_update_deliverable\` incrementally from the start.
8513
- 5. **Suggest follow-ups** \u2192 if work reveals more to do, suggest them (strongest when you have full context).
8514
- 6. **Complete** \u2192 \`deskfree_complete_task\` \u2014 deliverable required for "done".
8515
- 7. **Evaluate** \u2192 check \`pendingEvaluations\` and update ways of working.
8516
-
8517
- Key principles:
8518
- - You're building a chain. Your instructions become someone else's brief. Your deliverable becomes someone else's context.
8519
- - Write instructions as if briefing a contractor who has never seen the codebase.
8520
- - MUST update deliverable before completing with outcome "done".
8521
- - Estimate token cost per suggestion \u2014 consider files to read, reasoning, output.
8522
- - Sub-agents get 4 tools: update_deliverable, complete_task, send_message, submit_evaluation.`;
8523
- function getDeskFreeContext() {
8524
- return `${DESKFREE_AGENT_DIRECTIVE}
8801
+ var DESKFREE_AGENT_DIRECTIVE = `## DeskFree \u2014 Orchestrator
8802
+ You are the orchestrator. You assess state, propose plans, and dispatch work to sub-agents.
8803
+ 1. **Check state** \u2192 \`deskfree_state\` \u2014 see tasks, initiatives, ways of working.
8804
+ 2. **Propose plan** \u2192 \`deskfree_propose\` \u2014 ALL work requires human-approved tasks first.
8805
+ 3. **Dispatch** \u2192 spawn a sub-agent for each approved task. Pass the taskId \u2014 the sub-agent claims it.
8806
+ 4. **Communicate** \u2192 \`deskfree_send_message\` for updates outside task threads.
8807
+
8808
+ You do NOT claim tasks or do work directly. Sub-agents handle execution.
8809
+ - When a human writes in a task thread, you receive it with recent context. Use \`deskfree_reopen_task\` if it needs more work.
8810
+ - Write task instructions as if briefing a contractor who has never seen the codebase.
8811
+ - Estimate token cost per task \u2014 consider files to read, reasoning, output.
8812
+ - One initiative per proposal \u2014 make multiple calls for multiple initiatives.`;
8813
+ var DESKFREE_WORKER_DIRECTIVE = `## DeskFree Worker
8814
+ You are a worker sub-agent. Call \`deskfree_start_task\` with your taskId to claim and load context.
8815
+ Tools: deskfree_start_task, deskfree_update_file, deskfree_complete_task, deskfree_send_message, deskfree_propose.
8816
+ - Claim your task first with deskfree_start_task \u2014 this loads instructions, messages, and file context.
8817
+ - Save work to linked files with deskfree_update_file (incrementally).
8818
+ - Complete with deskfree_complete_task when done (summary required for outcome "done").
8819
+ - For evaluation tasks (mode="evaluation"), include the evaluation object in complete_task.
8820
+ - Propose follow-up tasks with deskfree_propose if you discover more work.`;
8821
+ function getDeskFreeContext(sessionKey) {
8822
+ const isWorker = sessionKey && (sessionKey.includes(":sub:") || sessionKey.includes(":spawn:") || sessionKey.includes(":run:"));
8823
+ const directive = isWorker ? DESKFREE_WORKER_DIRECTIVE : DESKFREE_AGENT_DIRECTIVE;
8824
+ return `${directive}
8525
8825
 
8526
8826
  <!-- deskfree-plugin:${PLUGIN_VERSION} -->`;
8527
8827
  }
@@ -8558,15 +8858,7 @@ function formatTaskResponse(task, summary, nextActions) {
8558
8858
  content: [
8559
8859
  {
8560
8860
  type: "text",
8561
- text: JSON.stringify(
8562
- {
8563
- summary,
8564
- nextActions,
8565
- task
8566
- },
8567
- null,
8568
- 2
8569
- )
8861
+ text: JSON.stringify({ summary, nextActions, task }, null, 2)
8570
8862
  }
8571
8863
  ]
8572
8864
  };
@@ -8577,11 +8869,7 @@ function formatConfirmation(summary, nextActions, data) {
8577
8869
  {
8578
8870
  type: "text",
8579
8871
  text: JSON.stringify(
8580
- {
8581
- summary,
8582
- nextActions,
8583
- ...data ? { data } : {}
8584
- },
8872
+ { summary, nextActions, ...data ? { data } : {} },
8585
8873
  null,
8586
8874
  2
8587
8875
  )
@@ -8642,14 +8930,7 @@ function errorResult(err) {
8642
8930
  content: [
8643
8931
  {
8644
8932
  type: "text",
8645
- text: JSON.stringify(
8646
- {
8647
- error: true,
8648
- message: rawMessage
8649
- },
8650
- null,
8651
- 2
8652
- )
8933
+ text: JSON.stringify({ error: true, message: rawMessage }, null, 2)
8653
8934
  }
8654
8935
  ],
8655
8936
  isError: true
@@ -8666,9 +8947,7 @@ function validateStringParam(params, key, required) {
8666
8947
  }
8667
8948
  const value = params[key];
8668
8949
  if (value == null) {
8669
- if (required) {
8670
- throw new Error(`Required parameter '${key}' is missing`);
8671
- }
8950
+ if (required) throw new Error(`Required parameter '${key}' is missing`);
8672
8951
  return void 0;
8673
8952
  }
8674
8953
  if (typeof value !== "string") {
@@ -8679,34 +8958,124 @@ function validateStringParam(params, key, required) {
8679
8958
  }
8680
8959
  return value;
8681
8960
  }
8682
- function validateEnumParam(params, key, allowedValues, required) {
8683
- if (!params) {
8684
- if (required) {
8685
- throw new Error(
8686
- `Required parameter '${key}' is missing (no params provided)`
8687
- );
8961
+ function parseProposeTasks(raw) {
8962
+ if (!Array.isArray(raw) || raw.length === 0) {
8963
+ throw new Error('Parameter "tasks" must be a non-empty array');
8964
+ }
8965
+ return raw.map((s, i) => {
8966
+ if (typeof s !== "object" || s === null) {
8967
+ throw new Error(`tasks[${i}] must be an object`);
8968
+ }
8969
+ const item = s;
8970
+ const title = item["title"];
8971
+ if (typeof title !== "string" || title.trim() === "") {
8972
+ throw new Error(`tasks[${i}].title must be a non-empty string`);
8973
+ }
8974
+ let substeps;
8975
+ if (Array.isArray(item["substeps"]) && item["substeps"].length > 0) {
8976
+ substeps = item["substeps"].filter((s2) => typeof s2 === "string" && s2.trim() !== "").map((s2) => s2.trim());
8977
+ if (substeps.length === 0) substeps = void 0;
8978
+ }
8979
+ let file;
8980
+ if (item["file"] && typeof item["file"] === "object") {
8981
+ const f = item["file"];
8982
+ if (typeof f["existingId"] === "string" && f["existingId"].trim()) {
8983
+ file = { type: "existing", existingId: f["existingId"].trim() };
8984
+ } else if (typeof f["name"] === "string" && f["name"].trim()) {
8985
+ file = {
8986
+ type: "new",
8987
+ name: f["name"].trim(),
8988
+ description: typeof f["description"] === "string" ? f["description"] : void 0
8989
+ };
8990
+ }
8688
8991
  }
8689
- return void 0;
8690
- }
8691
- const value = params[key];
8692
- if (value == null) {
8693
- if (required) {
8694
- throw new Error(`Required parameter '${key}' is missing`);
8992
+ return {
8993
+ title: title.trim(),
8994
+ instructions: typeof item["instructions"] === "string" ? item["instructions"] : void 0,
8995
+ substeps,
8996
+ file,
8997
+ estimatedTokens: typeof item["estimatedTokens"] === "number" ? item["estimatedTokens"] : void 0,
8998
+ scheduledFor: typeof item["scheduledFor"] === "string" ? item["scheduledFor"] : void 0
8999
+ };
9000
+ });
9001
+ }
9002
+ function parseInitiative(raw) {
9003
+ if (!raw) return void 0;
9004
+ if (typeof raw === "string") return raw;
9005
+ if (typeof raw === "object") {
9006
+ const obj = raw;
9007
+ if (typeof obj["title"] === "string" && obj["title"].trim()) {
9008
+ return {
9009
+ title: String(obj["title"]).trim(),
9010
+ content: String(obj["content"] ?? "")
9011
+ };
8695
9012
  }
8696
- return void 0;
8697
- }
8698
- if (typeof value !== "string") {
8699
- throw new Error(`Parameter '${key}' must be a string, got ${typeof value}`);
8700
- }
8701
- if (required && value.trim() === "") {
8702
- throw new Error(`Required parameter '${key}' cannot be empty`);
8703
- }
8704
- if (!allowedValues.includes(value)) {
8705
- throw new Error(
8706
- `Parameter '${key}' must be one of: ${allowedValues.join(", ")}, got: ${value}`
8707
- );
8708
9013
  }
8709
- return value;
9014
+ return void 0;
9015
+ }
9016
+ function makeReopenTaskHandler(client) {
9017
+ return async (_id, params) => {
9018
+ try {
9019
+ const taskId = validateStringParam(params, "taskId", true);
9020
+ const reason = validateStringParam(params, "reason", false);
9021
+ const result = await client.reopenTask({ taskId, reason });
9022
+ return formatTaskResponse(result, `Task "${result.title}" reopened`, [
9023
+ "Task is now in bot queue \u2014 spawn a sub-agent to claim it"
9024
+ ]);
9025
+ } catch (err) {
9026
+ return errorResult(err);
9027
+ }
9028
+ };
9029
+ }
9030
+ function makeSendMessageHandler(client) {
9031
+ return async (_id, params) => {
9032
+ try {
9033
+ const content = validateStringParam(params, "content", true);
9034
+ const taskId = validateStringParam(params, "taskId", false);
9035
+ await client.sendMessage({ content, taskId });
9036
+ return formatConfirmation(
9037
+ `Message sent${taskId ? ` to task ${taskId}` : ""}`,
9038
+ [
9039
+ "Message delivered to the human",
9040
+ taskId ? "Continue working on the task or wait for response" : "Check for response with task messages"
9041
+ ]
9042
+ );
9043
+ } catch (err) {
9044
+ return errorResult(err);
9045
+ }
9046
+ };
9047
+ }
9048
+ function makeProposeHandler(client) {
9049
+ return async (_id, params) => {
9050
+ try {
9051
+ const tasks = parseProposeTasks(params?.tasks);
9052
+ const context = validateStringParam(params, "context", false);
9053
+ const initiative = parseInitiative(params?.initiative);
9054
+ const result = await client.proposePlan({
9055
+ tasks: tasks.map((t) => ({
9056
+ title: t.title,
9057
+ instructions: t.instructions,
9058
+ substeps: t.substeps,
9059
+ file: t.file,
9060
+ estimatedTokens: t.estimatedTokens,
9061
+ scheduledFor: t.scheduledFor
9062
+ })),
9063
+ initiative,
9064
+ context: context ?? void 0
9065
+ });
9066
+ return formatConfirmation(
9067
+ `Proposed ${tasks.length} task${tasks.length === 1 ? "" : "s"} for human approval`,
9068
+ [
9069
+ "Plan stored as proposal \u2014 NO tasks created until human approves",
9070
+ "Human will review in the proposal modal and can edit/approve/reject each item",
9071
+ "Use deskfree_state to check for approved tasks"
9072
+ ],
9073
+ result
9074
+ );
9075
+ } catch (err) {
9076
+ return errorResult(err);
9077
+ }
9078
+ };
8710
9079
  }
8711
9080
  function createOrchestratorTools(api) {
8712
9081
  const account = resolveAccountFromConfig(api);
@@ -8727,336 +9096,16 @@ function createOrchestratorTools(api) {
8727
9096
  }
8728
9097
  },
8729
9098
  {
8730
- ...ORCHESTRATOR_TOOLS.START_TASK,
8731
- async execute(_id, params) {
8732
- try {
8733
- const taskId = validateStringParam(params, "taskId", true);
8734
- const result = await client.claimTask({ taskId });
8735
- setActiveTaskId(taskId);
8736
- return {
8737
- content: [
8738
- {
8739
- type: "text",
8740
- text: JSON.stringify(
8741
- {
8742
- summary: `Claimed task "${result.title}" \u2014 full context loaded`,
8743
- nextActions: [
8744
- "Read the instructions and message history carefully",
8745
- "Update deliverable incrementally with deskfree_update_deliverable",
8746
- "Complete with deskfree_complete_task when done"
8747
- ],
8748
- task: result
8749
- },
8750
- null,
8751
- 2
8752
- )
8753
- }
8754
- ]
8755
- };
8756
- } catch (err) {
8757
- return errorResult(err);
8758
- }
8759
- }
8760
- },
8761
- {
8762
- ...ORCHESTRATOR_TOOLS.UPDATE_DELIVERABLE,
8763
- async execute(_id, params) {
8764
- try {
8765
- const taskId = validateStringParam(params, "taskId", true);
8766
- const deliverable = validateStringParam(params, "deliverable", true);
8767
- const format = validateEnumParam(
8768
- params,
8769
- "format",
8770
- ["markdown", "html"],
8771
- false
8772
- );
8773
- await client.updateDeliverable({ taskId, deliverable, format });
8774
- return formatConfirmation(`Updated deliverable for task ${taskId}`, [
8775
- "Deliverable has been saved",
8776
- "Complete with deskfree_complete_task when ready"
8777
- ]);
8778
- } catch (err) {
8779
- return errorResult(err);
8780
- }
8781
- }
8782
- },
8783
- {
8784
- ...ORCHESTRATOR_TOOLS.COMPLETE_TASK,
8785
- async execute(_id, params) {
8786
- try {
8787
- const taskId = validateStringParam(params, "taskId", true);
8788
- const outcome = validateEnumParam(
8789
- params,
8790
- "outcome",
8791
- ["done", "blocked"],
8792
- true
8793
- );
8794
- const result = await client.completeTask({ taskId, outcome });
8795
- setActiveTaskId(null);
8796
- const summaryVerb = outcome === "done" ? "completed" : "blocked";
8797
- const icon = outcome === "done" ? "\u2705" : "\u{1F6AB}";
8798
- await client.sendMessage({
8799
- content: `${icon} Task ${summaryVerb}: "${result.title}"`
8800
- }).catch(() => {
8801
- });
8802
- return formatTaskResponse(
8803
- result,
8804
- `Task "${result.title}" marked as ${summaryVerb} \u2014 waiting for human`,
8805
- [
8806
- outcome === "done" ? "Human will review the deliverable" : "Human will review the blocker and provide guidance",
8807
- "Use deskfree_state to check for other tasks"
8808
- ]
8809
- );
8810
- } catch (err) {
8811
- return errorResult(err);
8812
- }
8813
- }
9099
+ ...ORCHESTRATOR_TOOLS.REOPEN_TASK,
9100
+ execute: makeReopenTaskHandler(client)
8814
9101
  },
8815
9102
  {
8816
9103
  ...ORCHESTRATOR_TOOLS.SEND_MESSAGE,
8817
- async execute(_id, params) {
8818
- try {
8819
- const content = validateStringParam(params, "content", false);
8820
- const taskId = validateStringParam(params, "taskId", false);
8821
- const rawSuggestions = params?.suggestions;
8822
- let suggestions;
8823
- if (Array.isArray(rawSuggestions) && rawSuggestions.length > 0) {
8824
- suggestions = rawSuggestions.map((t, i) => {
8825
- if (typeof t !== "object" || t === null) {
8826
- throw new Error(`suggestions[${i}] must be an object`);
8827
- }
8828
- const item = t;
8829
- const title = item["title"];
8830
- if (typeof title !== "string" || title.trim() === "") {
8831
- throw new Error(
8832
- `suggestions[${i}].title must be a non-empty string`
8833
- );
8834
- }
8835
- const instructions = item["instructions"];
8836
- return {
8837
- title: title.trim(),
8838
- instructions: typeof instructions === "string" ? instructions : void 0
8839
- };
8840
- });
8841
- }
8842
- if (!content && !suggestions) {
8843
- throw new Error(
8844
- 'Either "content" or "suggestions" parameter is required'
8845
- );
8846
- }
8847
- if (suggestions) {
8848
- await client.sendMessage({ suggestions, taskId });
8849
- return formatConfirmation(
8850
- `Suggested ${suggestions.length} task${suggestions.length === 1 ? "" : "s"} for human review`,
8851
- [
8852
- "The human will see approve/reject buttons for each suggestion",
8853
- "Continue working on the current task"
8854
- ]
8855
- );
8856
- }
8857
- await client.sendMessage({ content, taskId });
8858
- return formatConfirmation(
8859
- `Message sent${taskId ? ` to task ${taskId}` : ""}`,
8860
- [
8861
- "Message delivered to the human",
8862
- taskId ? "Continue working on the task or wait for response" : "Check for response with task messages"
8863
- ]
8864
- );
8865
- } catch (err) {
8866
- return errorResult(err);
8867
- }
8868
- }
8869
- },
8870
- {
8871
- ...ORCHESTRATOR_TOOLS.SUGGEST_TASKS,
8872
- async execute(_id, params) {
8873
- try {
8874
- const rawSuggestions = params?.suggestions;
8875
- if (!Array.isArray(rawSuggestions) || rawSuggestions.length === 0) {
8876
- throw new Error(
8877
- 'Parameter "suggestions" must be a non-empty array'
8878
- );
8879
- }
8880
- const suggestions = rawSuggestions.map((s, i) => {
8881
- if (typeof s !== "object" || s === null) {
8882
- throw new Error(`suggestions[${i}] must be an object`);
8883
- }
8884
- const item = s;
8885
- const title = item["title"];
8886
- if (typeof title !== "string" || title.trim() === "") {
8887
- throw new Error(
8888
- `suggestions[${i}].title must be a non-empty string`
8889
- );
8890
- }
8891
- return {
8892
- title: title.trim(),
8893
- instructions: typeof item["instructions"] === "string" ? item["instructions"] : void 0,
8894
- estimatedTokens: typeof item["estimatedTokens"] === "number" ? item["estimatedTokens"] : void 0,
8895
- dependsOn: Array.isArray(item["dependsOn"]) ? item["dependsOn"] : void 0,
8896
- initiativeId: typeof item["initiativeId"] === "string" ? item["initiativeId"] : void 0
8897
- };
8898
- });
8899
- const parentTaskId = validateStringParam(
8900
- params,
8901
- "parentTaskId",
8902
- false
8903
- );
8904
- const rawInitiativeSuggestions = params?.initiativeSuggestions;
8905
- let initiativeSuggestions;
8906
- if (Array.isArray(rawInitiativeSuggestions) && rawInitiativeSuggestions.length > 0) {
8907
- initiativeSuggestions = rawInitiativeSuggestions.map(
8908
- (s, i) => {
8909
- if (typeof s !== "object" || s === null) {
8910
- throw new Error(`initiativeSuggestions[${i}] must be an object`);
8911
- }
8912
- const item = s;
8913
- const title = item["title"];
8914
- if (typeof title !== "string" || title.trim() === "") {
8915
- throw new Error(
8916
- `initiativeSuggestions[${i}].title must be a non-empty string`
8917
- );
8918
- }
8919
- const content = item["content"];
8920
- if (typeof content !== "string") {
8921
- throw new Error(
8922
- `initiativeSuggestions[${i}].content must be a string`
8923
- );
8924
- }
8925
- return {
8926
- title: title.trim(),
8927
- content,
8928
- taskRefs: Array.isArray(item["taskRefs"]) ? item["taskRefs"] : void 0
8929
- };
8930
- }
8931
- );
8932
- }
8933
- const result = await client.suggestTasksDedicated({
8934
- suggestions,
8935
- parentTaskId,
8936
- initiativeSuggestions
8937
- });
8938
- const initiativeCount = initiativeSuggestions?.length ?? 0;
8939
- const summaryParts = [
8940
- `Suggested ${suggestions.length} task${suggestions.length === 1 ? "" : "s"} for human approval`,
8941
- ...initiativeCount > 0 ? [
8942
- `and ${initiativeCount} initiative${initiativeCount === 1 ? "" : "s"}`
8943
- ] : []
8944
- ];
8945
- return formatConfirmation(
8946
- summaryParts.join(" "),
8947
- [
8948
- 'Tasks created with "suggested" status \u2014 human will approve or reject each',
8949
- ...initiativeCount > 0 ? ["Initiative suggestions created \u2014 human will approve or reject independently"] : [],
8950
- "Use deskfree_state to check for approved tasks"
8951
- ],
8952
- result
8953
- );
8954
- } catch (err) {
8955
- return errorResult(err);
8956
- }
8957
- }
9104
+ execute: makeSendMessageHandler(client)
8958
9105
  },
8959
9106
  {
8960
- ...ORCHESTRATOR_TOOLS.CLAIM_EVALUATION,
8961
- async execute(_id, params) {
8962
- try {
8963
- const taskId = validateStringParam(params, "taskId", true);
8964
- const result = await client.claimEvaluation({ taskId });
8965
- if (!result) {
8966
- return formatConfirmation(
8967
- "Evaluation already claimed by another process",
8968
- ["Use deskfree_state to check for other pending evaluations"]
8969
- );
8970
- }
8971
- return {
8972
- content: [
8973
- {
8974
- type: "text",
8975
- text: JSON.stringify(
8976
- {
8977
- summary: `Claimed evaluation for task "${result.task.title}"`,
8978
- nextActions: [
8979
- "Review the task messages, current global ways of working, and initiative content (if present)",
8980
- "Decide what to update: globalWoW (universal patterns), initiative (area-specific learnings), both, or neither",
8981
- "Call deskfree_submit_evaluation with your analysis"
8982
- ],
8983
- task: result.task,
8984
- waysOfWorking: result.waysOfWorking,
8985
- currentVersion: result.currentVersion,
8986
- messages: result.messages,
8987
- ...result.initiative ? { initiative: result.initiative } : {}
8988
- },
8989
- null,
8990
- 2
8991
- )
8992
- }
8993
- ]
8994
- };
8995
- } catch (err) {
8996
- return errorResult(err);
8997
- }
8998
- }
8999
- },
9000
- {
9001
- ...ORCHESTRATOR_TOOLS.SUBMIT_EVALUATION,
9002
- async execute(_id, params) {
9003
- try {
9004
- const taskId = validateStringParam(params, "taskId", true);
9005
- const reasoning = validateStringParam(params, "reasoning", true);
9006
- const rawGlobalWoW = params?.globalWoW;
9007
- if (typeof rawGlobalWoW !== "object" || rawGlobalWoW === null) {
9008
- throw new Error('Parameter "globalWoW" must be an object');
9009
- }
9010
- const globalWoWObj = rawGlobalWoW;
9011
- if (typeof globalWoWObj["hasChanges"] !== "boolean") {
9012
- throw new Error('Parameter "globalWoW.hasChanges" must be a boolean');
9013
- }
9014
- const globalWoW = {
9015
- hasChanges: globalWoWObj["hasChanges"],
9016
- updatedContent: typeof globalWoWObj["updatedContent"] === "string" ? globalWoWObj["updatedContent"] : void 0
9017
- };
9018
- const rawInitiative = params?.initiative;
9019
- if (typeof rawInitiative !== "object" || rawInitiative === null) {
9020
- throw new Error('Parameter "initiative" must be an object');
9021
- }
9022
- const initiativeObj = rawInitiative;
9023
- if (typeof initiativeObj["hasChanges"] !== "boolean") {
9024
- throw new Error('Parameter "initiative.hasChanges" must be a boolean');
9025
- }
9026
- const initiative = {
9027
- hasChanges: initiativeObj["hasChanges"],
9028
- updatedContent: typeof initiativeObj["updatedContent"] === "string" ? initiativeObj["updatedContent"] : void 0
9029
- };
9030
- const result = await client.submitEvaluation({
9031
- taskId,
9032
- reasoning,
9033
- globalWoW,
9034
- initiative
9035
- });
9036
- const parts = [];
9037
- if (globalWoW.hasChanges) {
9038
- parts.push(`global WoW \u2192 v${result.globalVersion}`);
9039
- }
9040
- if (initiative.hasChanges && result.initiativeVersion !== void 0) {
9041
- parts.push(`initiative \u2192 v${result.initiativeVersion}`);
9042
- }
9043
- const messageContent = parts.length > 0 ? `\u{1F4DD} Updated ${parts.join(", ")}: ${reasoning}` : `\u{1F4DD} No updates to ways of working: ${reasoning}`;
9044
- await client.sendMessage({ content: messageContent, taskId }).catch(() => {
9045
- });
9046
- const summaryParts = [];
9047
- if (globalWoW.hasChanges) summaryParts.push(`global WoW v${result.globalVersion}`);
9048
- if (initiative.hasChanges && result.initiativeVersion !== void 0) {
9049
- summaryParts.push(`initiative v${result.initiativeVersion}`);
9050
- }
9051
- return formatConfirmation(
9052
- summaryParts.length > 0 ? `Evaluation complete \u2014 updated ${summaryParts.join(", ")}` : "Evaluation complete \u2014 no changes needed",
9053
- ["Use deskfree_state to check for other pending evaluations"],
9054
- result
9055
- );
9056
- } catch (err) {
9057
- return errorResult(err);
9058
- }
9059
- }
9107
+ ...ORCHESTRATOR_TOOLS.PROPOSE,
9108
+ execute: makeProposeHandler(client)
9060
9109
  }
9061
9110
  ];
9062
9111
  }
@@ -9177,8 +9226,8 @@ var plugin = {
9177
9226
  api.registerTool(() => {
9178
9227
  return createOrchestratorTools(api);
9179
9228
  });
9180
- api.on("before_agent_start", (_event, _ctx) => {
9181
- return { prependContext: getDeskFreeContext() };
9229
+ api.on("before_agent_start", (_event, ctx) => {
9230
+ return { prependContext: getDeskFreeContext(ctx.sessionKey) };
9182
9231
  });
9183
9232
  }
9184
9233
  };