assistme 0.8.10 → 0.8.11

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.
@@ -30,7 +30,7 @@ var CONFIG_DEFAULTS = {
30
30
  supabaseAnonKey: SUPABASE_ANON_KEY_DEFAULT,
31
31
  sessionName: "Default",
32
32
  model: "claude-sonnet-4-20250514",
33
- taskTimeoutMinutes: 10
33
+ taskTimeoutMinutes: 0
34
34
  };
35
35
  var config = new Conf({
36
36
  projectName: "assistme",
@@ -48,7 +48,7 @@ function getConfig() {
48
48
  workspacePath: resolve(workspacePath),
49
49
  sessionName: config.get("sessionName") || "Default",
50
50
  model: config.get("model") || "claude-sonnet-4-20250514",
51
- taskTimeoutMinutes: config.get("taskTimeoutMinutes") || 10
51
+ taskTimeoutMinutes: config.get("taskTimeoutMinutes") ?? 0
52
52
  };
53
53
  }
54
54
  function setConfig(key, value) {
@@ -17,13 +17,13 @@ import {
17
17
  readAuthStore,
18
18
  safeParse,
19
19
  writeAuthStore
20
- } from "./chunk-QGH5MFJA.js";
20
+ } from "./chunk-RUP2IQG3.js";
21
21
  import {
22
22
  AppError,
23
23
  errorMessage,
24
24
  getConfig,
25
25
  getDataDir
26
- } from "./chunk-HY3FFXSQ.js";
26
+ } from "./chunk-BGMIIZLF.js";
27
27
 
28
28
  // src/db/auth.ts
29
29
  async function loginWithToken(mcpToken) {
@@ -53,7 +53,7 @@ async function logout() {
53
53
 
54
54
  // src/db/session.ts
55
55
  async function createSession(sessionName, workspacePath, version) {
56
- const { getConfig: getConfig2 } = await import("./config-2HH7PO34.js");
56
+ const { getConfig: getConfig2 } = await import("./config-V2EJC3EH.js");
57
57
  const data = await callMcpHandler("session.create", {
58
58
  session_name: sessionName,
59
59
  workspace_path: workspacePath,
@@ -386,6 +386,8 @@ var BrowserController = class {
386
386
  // ── Navigation ──────────────────────────────────────────────────
387
387
  async navigate(url) {
388
388
  this.ensureConnected();
389
+ this.refCache.clear();
390
+ this.frameContexts.clear();
389
391
  await this.send("Page.navigate", { url });
390
392
  await this.waitForLoad();
391
393
  const info = await this.getPageInfo();
@@ -394,6 +396,8 @@ URL: ${info.url}`;
394
396
  }
395
397
  async goBack() {
396
398
  this.ensureConnected();
399
+ this.refCache.clear();
400
+ this.frameContexts.clear();
397
401
  try {
398
402
  const history = await this.send("Page.getNavigationHistory");
399
403
  const idx = history.currentIndex ?? 0;
@@ -414,6 +418,8 @@ URL: ${info.url}`;
414
418
  }
415
419
  async reload() {
416
420
  this.ensureConnected();
421
+ this.refCache.clear();
422
+ this.frameContexts.clear();
417
423
  await this.send("Page.reload");
418
424
  await this.waitForLoad();
419
425
  return "Page reloaded.";
@@ -1332,6 +1338,47 @@ Refs:
1332
1338
  return null;
1333
1339
  }
1334
1340
  }
1341
+ // ── Overlay Dismissal ────────────────────────────────────────────
1342
+ /**
1343
+ * Attempt to dismiss popups/overlays blocking interaction.
1344
+ * Tries Escape key first, then common close button patterns.
1345
+ * Called automatically when clickRef detects an element is covered.
1346
+ */
1347
+ async tryDismissOverlay() {
1348
+ try {
1349
+ await this.pressKey("Escape");
1350
+ await new Promise((r) => setTimeout(r, 300));
1351
+ await this.send("Runtime.evaluate", {
1352
+ expression: `
1353
+ (function() {
1354
+ var selectors = [
1355
+ 'button[aria-label="Close"]', 'button[aria-label="close"]',
1356
+ 'button[aria-label="Dismiss"]', 'button[aria-label="dismiss"]',
1357
+ '[role="dialog"] button[aria-label*="close" i]',
1358
+ '[role="dialog"] button[aria-label*="dismiss" i]',
1359
+ '[data-dismiss]', '[data-close]',
1360
+ '.modal-close', '[class*="close-button"]',
1361
+ '[class*="CloseButton"]', '[class*="dismiss"]',
1362
+ ];
1363
+ for (var i = 0; i < selectors.length; i++) {
1364
+ try {
1365
+ var btn = document.querySelector(selectors[i]);
1366
+ if (btn && btn.offsetParent !== null) {
1367
+ btn.click();
1368
+ return 'clicked';
1369
+ }
1370
+ } catch(e) {}
1371
+ }
1372
+ return 'none';
1373
+ })()
1374
+ `,
1375
+ returnByValue: true
1376
+ });
1377
+ return true;
1378
+ } catch {
1379
+ return false;
1380
+ }
1381
+ }
1335
1382
  // ── Ref-based Interactions (CDP Input Events) ─────────────────────
1336
1383
  /**
1337
1384
  * Click an element by ref using CDP Input.dispatchMouseEvent.
@@ -1342,6 +1389,8 @@ Refs:
1342
1389
  * Includes auto-wait: retries up to 3 times (with 500ms intervals) if the
1343
1390
  * element is not yet actionable (e.g., covered by a loading overlay, still
1344
1391
  * animating into view). This matches Playwright's auto-waiting behavior.
1392
+ * When an element is covered by an overlay, auto-dismiss is attempted
1393
+ * (Escape key + common close buttons) before retrying.
1345
1394
  */
1346
1395
  async clickRef(refId) {
1347
1396
  this.ensureConnected();
@@ -1359,6 +1408,11 @@ Refs:
1359
1408
  }
1360
1409
  if (resolved.error) {
1361
1410
  lastError = resolved.error;
1411
+ if (resolved.error.includes("Element is covered by") && attempt < maxRetries - 1) {
1412
+ await this.tryDismissOverlay();
1413
+ await new Promise((r) => setTimeout(r, 500));
1414
+ continue;
1415
+ }
1362
1416
  if (attempt < maxRetries - 1) {
1363
1417
  await new Promise((r) => setTimeout(r, 500));
1364
1418
  continue;
@@ -1542,7 +1596,14 @@ Refs:
1542
1596
  result,
1543
1597
  success
1544
1598
  });
1545
- if (!success) break;
1599
+ if (!success) {
1600
+ results.push({
1601
+ action: "hint",
1602
+ result: "Refs may be stale after failure. Use browser_snapshot to get fresh refs before retrying.",
1603
+ success: true
1604
+ });
1605
+ break;
1606
+ }
1546
1607
  if (spec.action !== "wait") {
1547
1608
  await new Promise((r) => setTimeout(r, 200));
1548
1609
  }
@@ -2,7 +2,7 @@ import {
2
2
  errorMessage,
3
3
  getConfig,
4
4
  getDataDir
5
- } from "./chunk-HY3FFXSQ.js";
5
+ } from "./chunk-BGMIIZLF.js";
6
6
 
7
7
  // src/db/auth-store.ts
8
8
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -240,6 +240,7 @@ var JobRowSchema = z.object({
240
240
  job_id: z.string(),
241
241
  job_name: z.string(),
242
242
  job_description: z.string().optional().default(""),
243
+ job_prompt: z.string().optional().nullable(),
243
244
  skill_id: z.string().optional().nullable(),
244
245
  skill_name: z.string().optional().nullable(),
245
246
  skill_description: z.string().optional().default(""),
@@ -250,6 +251,7 @@ var JobListRowSchema = z.object({
250
251
  id: z.string(),
251
252
  name: z.string(),
252
253
  description: z.string().optional().default(""),
254
+ prompt: z.string().optional().nullable(),
253
255
  skill_count: z.number().optional().default(0)
254
256
  });
255
257
  var JobRunRowSchema = z.object({
@@ -373,6 +375,7 @@ var JobRunner = class {
373
375
  jobId: first.job_id,
374
376
  jobName: first.job_name,
375
377
  jobDescription: first.job_description,
378
+ jobPrompt: first.job_prompt ?? null,
376
379
  skills: rows.filter((row) => row.skill_id).map((row) => ({
377
380
  skillId: row.skill_id,
378
381
  skillName: row.skill_name || "",
@@ -469,7 +472,12 @@ var JobRunner = class {
469
472
  prompt += `**Name:** ${job.jobName}
470
473
  `;
471
474
  prompt += `**Description:** ${job.jobDescription}
472
-
475
+ `;
476
+ if (job.jobPrompt) {
477
+ prompt += `**Current Execution Prompt:** ${job.jobPrompt}
478
+ `;
479
+ }
480
+ prompt += `
473
481
  `;
474
482
  if (job.skills.length > 0) {
475
483
  prompt += `**Current Skills:**
@@ -510,9 +518,10 @@ var JobRunner = class {
510
518
  * chain them based on what it discovers at runtime.
511
519
  */
512
520
  buildJobPrompt(job, runId) {
521
+ const effectiveDescription = job.jobPrompt || job.jobDescription;
513
522
  let prompt = `## Job: ${job.jobName}
514
523
  `;
515
- prompt += `*${job.jobDescription}*
524
+ prompt += `*${effectiveDescription}*
516
525
 
517
526
  `;
518
527
  prompt += `**Run ID:** ${runId}
@@ -536,12 +545,17 @@ var JobRunner = class {
536
545
  }
537
546
  prompt += `
538
547
  ### How to Work
548
+ `;
549
+ prompt += `- **Plan first**: Before jumping into actions, briefly plan your approach \u2014 which sites/tools to use, in what order, and how to verify results.
539
550
  `;
540
551
  prompt += `- **Be agentic**: Decide what to do based on what you discover. `;
541
552
  prompt += `If checking Slack reveals a request that requires GitHub work, go do the GitHub work immediately \u2014 don't just note it for later.
542
553
  `;
543
554
  prompt += `- **Chain dynamically**: One skill's output should inform your next action. `;
544
555
  prompt += `For example, if you find an assigned GitHub issue, use your coding skills to implement it.
556
+ `;
557
+ prompt += `- **Use separate tabs for different sites**: When working across multiple websites, open each in its own tab (browser_new_tab). `;
558
+ prompt += `This preserves page state and avoids re-navigation. Use browser_switch_tab to move between them.
545
559
  `;
546
560
  prompt += `- **Skip what's irrelevant**: If a capability doesn't apply right now, skip it.
547
561
  `;
@@ -550,6 +564,8 @@ var JobRunner = class {
550
564
  `;
551
565
  prompt += `- **Respond and act**: If you find messages or issues that need replies, reply to them. `;
552
566
  prompt += `If you find code tasks, implement them.
567
+ `;
568
+ prompt += `- **Handle auth walls**: If a site requires login, use browser_request_user_action immediately \u2014 do not waste attempts trying to bypass auth walls.
553
569
 
554
570
  `;
555
571
  prompt += `When finished, provide a summary of what you accomplished and any items that need the user's attention.
@@ -7,7 +7,7 @@ import {
7
7
  getDataDir,
8
8
  resetDirCaches,
9
9
  setConfig
10
- } from "./chunk-HY3FFXSQ.js";
10
+ } from "./chunk-BGMIIZLF.js";
11
11
  export {
12
12
  assertWithinAssistMeRoot,
13
13
  clearConfig,
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  setSessionBusy,
28
28
  toggleScheduledTask,
29
29
  updateHeartbeat
30
- } from "./chunk-T3DBLWUW.js";
30
+ } from "./chunk-RJBLIGFJ.js";
31
31
  import {
32
32
  HEARTBEAT_INTERVAL_MS,
33
33
  HEARTBEAT_LOG_MAX_ENTRIES,
@@ -39,7 +39,7 @@ import {
39
39
  setLogConversationId,
40
40
  setLogHook,
41
41
  setLogLevel
42
- } from "./chunk-QGH5MFJA.js";
42
+ } from "./chunk-RUP2IQG3.js";
43
43
  import {
44
44
  clearConfig,
45
45
  errorMessage,
@@ -47,7 +47,7 @@ import {
47
47
  getConfigPath,
48
48
  getDataDir,
49
49
  setConfig
50
- } from "./chunk-HY3FFXSQ.js";
50
+ } from "./chunk-BGMIIZLF.js";
51
51
 
52
52
  // src/index.ts
53
53
  import { Command } from "commander";
@@ -1500,12 +1500,10 @@ var Orchestrator = class {
1500
1500
  await this.dispatchAndWait(`[JobRun: ${jobRun.job_name}] ${prompt}`);
1501
1501
  await runner.completeRun(jobRun.id, "completed", "Job executed via web trigger");
1502
1502
  } catch (err) {
1503
- log.error(`Job run failed: ${err}`);
1504
- await runner.completeRun(
1505
- jobRun.id,
1506
- "failed",
1507
- `Execution error: ${err instanceof Error ? err.message : err}`
1508
- ).catch((e) => log.error(`Failed to mark run as failed: ${e}`));
1503
+ const errMsg = err instanceof Error ? err.message : String(err);
1504
+ log.error(`Job run failed: ${errMsg}`);
1505
+ const summary = errMsg.includes("cancelled") ? `Cancelled by user. Check workspace for any partial results.` : `Execution error: ${errMsg}`;
1506
+ await runner.completeRun(jobRun.id, "failed", summary).catch((e) => log.error(`Failed to mark run as failed: ${e}`));
1509
1507
  }
1510
1508
  }
1511
1509
  // ── Busy State ──────────────────────────────────────────────────
@@ -2237,7 +2235,7 @@ function registerJobCommands(program2) {
2237
2235
  jobCmd.command("list").description("List your defined jobs").action(async () => {
2238
2236
  try {
2239
2237
  await getCurrentUserId();
2240
- const { JobRunner: JobRunner2 } = await import("./job-runner-IBVUDW6A.js");
2238
+ const { JobRunner: JobRunner2 } = await import("./job-runner-OT3HZQEF.js");
2241
2239
  const runner = new JobRunner2();
2242
2240
  const jobs = await runner.listJobs();
2243
2241
  if (jobs.length === 0) {
@@ -2261,7 +2259,7 @@ function registerJobCommands(program2) {
2261
2259
  jobCmd.command("status [name]").description("Show run history for a job (or all jobs)").option("-l, --limit <number>", "Max runs to show (default: 5)").action(async (name, opts) => {
2262
2260
  try {
2263
2261
  await getCurrentUserId();
2264
- const { JobRunner: JobRunner2 } = await import("./job-runner-IBVUDW6A.js");
2262
+ const { JobRunner: JobRunner2 } = await import("./job-runner-OT3HZQEF.js");
2265
2263
  const runner = new JobRunner2();
2266
2264
  const runs = await runner.getRunHistory(name, parseInt(opts.limit || "5"));
2267
2265
  if (runs.length === 0) {
@@ -2300,7 +2298,7 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`));
2300
2298
  process.exit(1);
2301
2299
  }
2302
2300
  await getCurrentUserId();
2303
- const { JobRunner: JobRunner2 } = await import("./job-runner-IBVUDW6A.js");
2301
+ const { JobRunner: JobRunner2 } = await import("./job-runner-OT3HZQEF.js");
2304
2302
  const runner = new JobRunner2();
2305
2303
  const job = await runner.loadJob(name);
2306
2304
  if (!job) {
@@ -0,0 +1,7 @@
1
+ import {
2
+ JobRunner
3
+ } from "./chunk-RUP2IQG3.js";
4
+ import "./chunk-BGMIIZLF.js";
5
+ export {
6
+ JobRunner
7
+ };
@@ -20,7 +20,7 @@ import {
20
20
  resetEventSequence,
21
21
  setActionRequest,
22
22
  upsertAgentSkill
23
- } from "../chunk-T3DBLWUW.js";
23
+ } from "../chunk-RJBLIGFJ.js";
24
24
  import {
25
25
  EDSGER_PRODUCT_SLUG,
26
26
  JobRunner,
@@ -47,14 +47,14 @@ import {
47
47
  safeParse,
48
48
  setCorrelationId,
49
49
  setLogTransport
50
- } from "../chunk-QGH5MFJA.js";
50
+ } from "../chunk-RUP2IQG3.js";
51
51
  import {
52
52
  AppError,
53
53
  assertWithinAssistMeRoot,
54
54
  errorMessage,
55
55
  getAssistMeRoot,
56
56
  getConfig
57
- } from "../chunk-HY3FFXSQ.js";
57
+ } from "../chunk-BGMIIZLF.js";
58
58
 
59
59
  // src/workers/conversation.ts
60
60
  import { config as loadEnv } from "dotenv";
@@ -1708,7 +1708,9 @@ function createAgentToolsServer(deps) {
1708
1708
  {
1709
1709
  name: z2.string().describe("Skill name in kebab-case, e.g. 'flight-booking'"),
1710
1710
  description: z2.string().describe("One-line description of what this skill does"),
1711
- instructions: z2.string().describe("Markdown step-by-step instructions"),
1711
+ instructions: z2.string().describe(
1712
+ "Markdown step-by-step instructions. IMPORTANT: When a skill produces document output (reports, summaries, etc.), instruct it to generate .docx/.xlsx/.pptx files (not .md), so end users can open and edit them."
1713
+ ),
1712
1714
  emoji: z2.string().optional().describe("Single emoji representing this skill")
1713
1715
  },
1714
1716
  async (args) => {
@@ -1984,14 +1986,38 @@ Use \`ask_user\` to request these from the user, or create them yourself (e.g. r
1984
1986
 
1985
1987
  `;
1986
1988
  }
1987
- response += `**Your task:** Analyze this job description and decompose it into 4-10 automatable skills.
1989
+ response += `**Your task:** Analyze this job description and do TWO things:
1990
+ `;
1991
+ response += `- **A. Rewrite the description into a professional, structured prompt** that clearly defines the job's objectives, scope, and expected outcomes. This will be used as the actual execution prompt when running the job. Store it as the \`prompt\` parameter when calling \`skill_link_job\`.
1992
+ `;
1993
+ response += `- **B. Decompose the job into 4-10 automatable skills.**
1988
1994
 
1989
1995
  `;
1990
- response += `**IMPORTANT \u2014 You MUST use ask_user before creating skills:**
1996
+ response += `**Example \u2014 Task A (professional prompt rewrite):**
1997
+ `;
1998
+ response += `> User description: "\u6211\u662F\u7535\u5546\u8FD0\u8425\uFF0C\u6BCF\u5929\u770B\u7ADE\u54C1\u4EF7\u683C\u3001\u5199\u6587\u6848\u3001\u56DE\u590D\u8BC4\u8BBA"
1999
+ `;
2000
+ response += `> Professional prompt:
1991
2001
  `;
1992
- response += `1. Analyze the job and draft a list of proposed skills (name, emoji, one-line description for each).
2002
+ response += `> \u4F5C\u4E3A\u7535\u5546\u8FD0\u8425\u4E13\u5458\uFF0C\u7CFB\u7EDF\u6027\u5730\u6267\u884C\u4EE5\u4E0B\u6838\u5FC3\u65E5\u5E38\u804C\u8D23\uFF1A
1993
2003
  `;
1994
- response += `2. Call \`ask_user\` with the formatted skill list as "question" and these options:
2004
+ response += `> 1. \u7ADE\u54C1\u4EF7\u683C\u76D1\u63A7 \u2014 \u91C7\u96C6\u4E3B\u8981\u7ADE\u54C1\u5728\u5404\u7535\u5546\u5E73\u53F0\u7684\u5B9E\u65F6\u4EF7\u683C\uFF0C\u8BC6\u522B\u4EF7\u683C\u53D8\u52A8\u8D8B\u52BF\uFF0C\u751F\u6210\u5BF9\u6BD4\u62A5\u544A\uFF08.docx\uFF09
2005
+ `;
2006
+ response += `> 2. \u5546\u54C1\u6587\u6848\u64B0\u5199 \u2014 \u6839\u636E\u4EA7\u54C1\u5356\u70B9\u3001\u76EE\u6807\u53D7\u4F17\u548C\u5E73\u53F0\u8C03\u6027\uFF0C\u64B0\u5199\u7B26\u5408 SEO \u89C4\u8303\u7684\u5546\u54C1\u6807\u9898\u4E0E\u63CF\u8FF0
2007
+ `;
2008
+ response += `> 3. \u5BA2\u6237\u8BC4\u8BBA\u7BA1\u7406 \u2014 \u68C0\u67E5\u65B0\u589E\u5DEE\u8BC4\u4E0E\u54A8\u8BE2\uFF0C\u6309\u4F18\u5148\u7EA7\u5206\u7C7B\uFF0C\u8D77\u8349\u4E13\u4E1A\u56DE\u590D
2009
+ `;
2010
+ response += `> \u5B8C\u6210\u540E\u6C47\u603B\u5F53\u65E5\u5DE5\u4F5C\u6458\u8981\uFF0C\u6807\u6CE8\u9700\u8981\u4EBA\u5DE5\u5173\u6CE8\u7684\u4E8B\u9879\u3002
2011
+
2012
+ `;
2013
+ response += `The prompt should be in the same language as the user's description. Keep it under 2000 characters.
2014
+
2015
+ `;
2016
+ response += `**Workflow (follow these steps in order):**
2017
+ `;
2018
+ response += `1. Draft the professional prompt (task A) AND a list of proposed skills with name, emoji, one-line description each (task B).
2019
+ `;
2020
+ response += `2. Call \`ask_user\` with both the rewritten prompt and the skill list as "question", and these options:
1995
2021
  `;
1996
2022
  response += ` - options: [{label: "Approve All", action_key: "approve_all", description: "Create all proposed skills"}, {label: "Cancel", action_key: "cancel", description: "Do not create any skills"}]
1997
2023
  `;
@@ -2014,7 +2040,7 @@ Use \`ask_user\` to request these from the user, or create them yourself (e.g. r
2014
2040
  response += `skill_create automatically adds the skill to the user's collection \u2014 no need to call skill_add.
2015
2041
 
2016
2042
  `;
2017
- response += `After ALL skills are created, call \`skill_link_job\` with job_name="${args.job_name}" and the list of created skill names to link them and mark the job as analyzed.
2043
+ response += `After ALL skills are created, call \`skill_link_job\` with job_name="${args.job_name}", the list of created skill names, AND the professional prompt you drafted to link them and mark the job as analyzed.
2018
2044
 
2019
2045
  `;
2020
2046
  response += `**Guidelines for skill instructions:**
@@ -2028,6 +2054,8 @@ Use \`ask_user\` to request these from the user, or create them yourself (e.g. r
2028
2054
  response += `- Use placeholders like {query}, {date} for variable inputs
2029
2055
  `;
2030
2056
  response += `- Each skill should be a single, well-defined workflow (10-25 steps)
2057
+ `;
2058
+ response += `- When a skill produces document output (reports, summaries, data exports), instruct it to generate .docx/.xlsx/.pptx files \u2014 NOT .md \u2014 so end users can open and edit them
2031
2059
  `;
2032
2060
  return { content: [{ type: "text", text: response }] };
2033
2061
  }
@@ -2038,11 +2066,14 @@ Use \`ask_user\` to request these from the user, or create them yourself (e.g. r
2038
2066
  {
2039
2067
  job_name: z2.string().describe("Name of the job to link skills to"),
2040
2068
  job_description: z2.string().describe("Job description (used if job doesn't exist yet)"),
2041
- skill_names: z2.array(z2.string()).describe("Names of skills to link to this job")
2069
+ skill_names: z2.array(z2.string()).describe("Names of skills to link to this job"),
2070
+ prompt: z2.string().optional().describe(
2071
+ "Professional, structured rewrite of the job description (max 2000 chars). This becomes the actual execution prompt when running the job. Should clearly define objectives, scope, and expected outcomes."
2072
+ )
2042
2073
  },
2043
2074
  async (args) => {
2044
2075
  try {
2045
- await saveJobToDb(args.job_name, args.job_description, args.skill_names);
2076
+ await saveJobToDb(args.job_name, args.job_description, args.skill_names, args.prompt);
2046
2077
  log.success(
2047
2078
  `Job "${args.job_name}": linked ${args.skill_names.length} skills and marked as analyzed`
2048
2079
  );
@@ -2851,12 +2882,13 @@ ${message}`
2851
2882
  ]
2852
2883
  });
2853
2884
  }
2854
- async function saveJobToDb(jobName, jobDescription, createdSkillNames) {
2885
+ async function saveJobToDb(jobName, jobDescription, createdSkillNames, prompt) {
2855
2886
  try {
2856
2887
  const data = await callMcpHandler("job.save_with_skills", {
2857
2888
  job_name: jobName,
2858
2889
  job_description: jobDescription,
2859
- skill_names: createdSkillNames
2890
+ skill_names: createdSkillNames,
2891
+ prompt: prompt || null
2860
2892
  });
2861
2893
  log.debug(
2862
2894
  `Job "${jobName}" saved via edge function (id: ${data}), ${createdSkillNames.length} skill(s) linked`
@@ -2921,12 +2953,21 @@ function createEventHooks(taskId, toolCallRecords, toolFailures = []) {
2921
2953
  error: errorStr.slice(0, 500),
2922
2954
  timestamp: Date.now()
2923
2955
  });
2956
+ const sameToolFailures = toolFailures.filter((f) => f.toolName === displayName).length;
2957
+ const isBrowser = displayName.startsWith("browser_");
2958
+ const browserFailures = isBrowser ? toolFailures.filter((f) => f.toolName.startsWith("browser_")).length : 0;
2924
2959
  await emitEvent(taskId, "tool_failure", {
2925
2960
  name: displayName,
2926
2961
  error: errorStr.slice(0, 500),
2927
- failure_count: toolFailures.filter((f) => f.toolName === displayName).length
2962
+ failure_count: sameToolFailures,
2963
+ browser_failure_count: browserFailures
2928
2964
  });
2929
2965
  log.warn(`Tool failure tracked: ${displayName} (total: ${toolFailures.length})`);
2966
+ if (isBrowser && browserFailures >= 3 && browserFailures % 3 === 0) {
2967
+ await emitEvent(taskId, "tool_failure_hint", {
2968
+ message: `${browserFailures} browser tool failures detected. Consider: (1) re-snapshot to get fresh refs, (2) use separate tabs for different sites, (3) navigate directly to target URLs to bypass popups, (4) use browser_request_user_action for login walls.`
2969
+ });
2970
+ }
2930
2971
  return {};
2931
2972
  };
2932
2973
  return {
@@ -3124,6 +3165,13 @@ Workflow for web tasks (e.g. "\u67E5\u4E00\u4E0B kindle \u6700\u65B0\u6B3E\u4EF7
3124
3165
  6. Repeat 4-5 as needed (re-snapshot after navigation or major page changes)
3125
3166
  7. Summarize findings
3126
3167
 
3168
+ Workflow for MULTI-SITE tasks (e.g. "\u5728 Seek \u627E\u5DE5\u4F5C\uFF0C\u7136\u540E\u53BB Immigration NZ \u9A8C\u8BC1\u96C7\u4E3B"):
3169
+ - ALWAYS use separate tabs for different websites. Do NOT navigate back and forth in one tab \u2014 you lose page state.
3170
+ - Open each site in its own tab: browser_new_tab \u2192 browser_navigate
3171
+ - Use browser_switch_tab to switch between sites
3172
+ - This preserves scroll position, form data, and search results on each site
3173
+ - Example: Tab 0 = Seek (job search), Tab 1 = Immigration NZ (employer check), Tab 2 = LinkedIn (research)
3174
+
3127
3175
  Workflow for form filling (e.g. "\u6CE8\u518C\u4E00\u4E2A Gmail \u8D26\u53F7"):
3128
3176
  1. browser_connect + browser_navigate \u2192 go to the form page
3129
3177
  2. browser_snapshot \u2192 see all form fields with ref numbers
@@ -3140,6 +3188,21 @@ Workflow for form filling (e.g. "\u6CE8\u518C\u4E00\u4E2A Gmail \u8D26\u53F7"):
3140
3188
  - Browser approach too slow \u2192 consider building a script/CLI solution
3141
3189
  - One data source fails \u2192 try an alternative source
3142
3190
  - If stuck after 2 failed attempts at the same step, try a fundamentally different approach
3191
+ - **Popup/overlay blocks interaction** \u2192 The system auto-tries Escape + close buttons. If that fails, navigate directly to the target URL (e.g. append search params to skip the homepage popup). Do NOT repeatedly try to click through the same overlay.
3192
+ - **Login/auth wall** \u2192 Use browser_request_user_action to ask the user to log in. Use memory_store to remember which sites require login so you can warn the user upfront next time.
3193
+ - **Stale refs after navigation** \u2192 Refs are automatically cleared on navigation. Always re-snapshot after browser_navigate, goBack, or reload. If browser_act returns a "hint" about stale refs, re-snapshot immediately.
3194
+
3195
+ 10. DOCUMENT OUTPUT FORMAT:
3196
+ - When creating documents, reports, or deliverables for the user, ALWAYS prefer Microsoft Office formats (.docx for text documents, .xlsx for spreadsheets, .pptx for presentations) over Markdown (.md).
3197
+ - Regular users need to open, read, and edit these files \u2014 Office formats are universally accessible.
3198
+ - Use the Bash tool to install and run document-generation libraries as needed:
3199
+ * Python: python-docx (Word), openpyxl (Excel), python-pptx (PowerPoint)
3200
+ * Node.js: docx (Word), exceljs (Excel), pptx (PowerPoint)
3201
+ - Only use Markdown when:
3202
+ * The output is internal (e.g. README.md in a program directory, skill instructions)
3203
+ * The user explicitly requests Markdown
3204
+ * The content is a quick inline response in the conversation (not a standalone file)
3205
+ - When writing .docx files, include proper formatting: headings, paragraphs, tables, bullet lists \u2014 not just plain text dumped into a Word file.
3143
3206
 
3144
3207
  Guidelines:
3145
3208
  - SELF-VERIFY before finishing: re-read modified files, take a final screenshot after browser actions, or re-check output to confirm correctness. Never assume success without confirming the end state.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistme",
3
- "version": "0.8.10",
3
+ "version": "0.8.11",
4
4
  "description": "AssistMe CLI Agent - AI-powered agentic assistant for code, browser, and automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -34,7 +34,6 @@ export function stripMcpPrefix(toolName: string): string {
34
34
  return match ? match[1] : toolName;
35
35
  }
36
36
 
37
-
38
37
  /**
39
38
  * Create PreToolUse and PostToolUse hooks that emit events to Supabase.
40
39
  * These hooks let the web UI display tool activity in real-time.
@@ -113,14 +112,28 @@ export function createEventHooks(
113
112
  timestamp: Date.now(),
114
113
  });
115
114
 
115
+ const sameToolFailures = toolFailures.filter((f) => f.toolName === displayName).length;
116
+ const isBrowser = displayName.startsWith("browser_");
117
+ const browserFailures = isBrowser
118
+ ? toolFailures.filter((f) => f.toolName.startsWith("browser_")).length
119
+ : 0;
120
+
116
121
  await emitEvent(taskId, "tool_failure", {
117
122
  name: displayName,
118
123
  error: errorStr.slice(0, 500),
119
- failure_count: toolFailures.filter((f) => f.toolName === displayName).length,
124
+ failure_count: sameToolFailures,
125
+ browser_failure_count: browserFailures,
120
126
  });
121
127
 
122
128
  log.warn(`Tool failure tracked: ${displayName} (total: ${toolFailures.length})`);
123
129
 
130
+ // Emit strategy hint when browser failures accumulate
131
+ if (isBrowser && browserFailures >= 3 && browserFailures % 3 === 0) {
132
+ await emitEvent(taskId, "tool_failure_hint", {
133
+ message: `${browserFailures} browser tool failures detected. Consider: (1) re-snapshot to get fresh refs, (2) use separate tabs for different sites, (3) navigate directly to target URLs to bypass popups, (4) use browser_request_user_action for login walls.`,
134
+ });
135
+ }
136
+
124
137
  return {};
125
138
  };
126
139
 
@@ -18,6 +18,7 @@ export interface JobInfo {
18
18
  jobId: string;
19
19
  jobName: string;
20
20
  jobDescription: string;
21
+ jobPrompt: string | null;
21
22
  skills: JobSkillInfo[];
22
23
  }
23
24
 
@@ -57,6 +58,7 @@ export class JobRunner {
57
58
  jobId: first.job_id,
58
59
  jobName: first.job_name,
59
60
  jobDescription: first.job_description,
61
+ jobPrompt: first.job_prompt ?? null,
60
62
  skills: rows
61
63
  .filter((row) => row!.skill_id)
62
64
  .map((row) => ({
@@ -172,7 +174,11 @@ export class JobRunner {
172
174
  let prompt = `Analyze this job definition and determine what skills are needed to execute it effectively.\n\n`;
173
175
  prompt += `## Job Definition\n`;
174
176
  prompt += `**Name:** ${job.jobName}\n`;
175
- prompt += `**Description:** ${job.jobDescription}\n\n`;
177
+ prompt += `**Description:** ${job.jobDescription}\n`;
178
+ if (job.jobPrompt) {
179
+ prompt += `**Current Execution Prompt:** ${job.jobPrompt}\n`;
180
+ }
181
+ prompt += `\n`;
176
182
 
177
183
  if (job.skills.length > 0) {
178
184
  prompt += `**Current Skills:**\n`;
@@ -204,8 +210,9 @@ export class JobRunner {
204
210
  * chain them based on what it discovers at runtime.
205
211
  */
206
212
  buildJobPrompt(job: JobInfo, runId: string): string {
213
+ const effectiveDescription = job.jobPrompt || job.jobDescription;
207
214
  let prompt = `## Job: ${job.jobName}\n`;
208
- prompt += `*${job.jobDescription}*\n\n`;
215
+ prompt += `*${effectiveDescription}*\n\n`;
209
216
  prompt += `**Run ID:** ${runId}\n\n`;
210
217
 
211
218
  prompt += `You are now acting as "${job.jobName}". `;
@@ -222,15 +229,19 @@ export class JobRunner {
222
229
  }
223
230
 
224
231
  prompt += `\n### How to Work\n`;
232
+ prompt += `- **Plan first**: Before jumping into actions, briefly plan your approach — which sites/tools to use, in what order, and how to verify results.\n`;
225
233
  prompt += `- **Be agentic**: Decide what to do based on what you discover. `;
226
234
  prompt += `If checking Slack reveals a request that requires GitHub work, go do the GitHub work immediately — don't just note it for later.\n`;
227
235
  prompt += `- **Chain dynamically**: One skill's output should inform your next action. `;
228
236
  prompt += `For example, if you find an assigned GitHub issue, use your coding skills to implement it.\n`;
237
+ prompt += `- **Use separate tabs for different sites**: When working across multiple websites, open each in its own tab (browser_new_tab). `;
238
+ prompt += `This preserves page state and avoids re-navigation. Use browser_switch_tab to move between them.\n`;
229
239
  prompt += `- **Skip what's irrelevant**: If a capability doesn't apply right now, skip it.\n`;
230
240
  prompt += `- **Use tools directly too**: You also have browser, file, and shell tools. `;
231
241
  prompt += `Use them directly when a skill isn't needed — skills are guides, not mandatory steps.\n`;
232
242
  prompt += `- **Respond and act**: If you find messages or issues that need replies, reply to them. `;
233
- prompt += `If you find code tasks, implement them.\n\n`;
243
+ prompt += `If you find code tasks, implement them.\n`;
244
+ prompt += `- **Handle auth walls**: If a site requires login, use browser_request_user_action immediately — do not waste attempts trying to bypass auth walls.\n\n`;
234
245
 
235
246
  prompt += `When finished, provide a summary of what you accomplished and any items that need the user's attention.\n`;
236
247
 
@@ -147,6 +147,13 @@ Workflow for web tasks (e.g. "查一下 kindle 最新款价格"):
147
147
  6. Repeat 4-5 as needed (re-snapshot after navigation or major page changes)
148
148
  7. Summarize findings
149
149
 
150
+ Workflow for MULTI-SITE tasks (e.g. "在 Seek 找工作,然后去 Immigration NZ 验证雇主"):
151
+ - ALWAYS use separate tabs for different websites. Do NOT navigate back and forth in one tab — you lose page state.
152
+ - Open each site in its own tab: browser_new_tab → browser_navigate
153
+ - Use browser_switch_tab to switch between sites
154
+ - This preserves scroll position, form data, and search results on each site
155
+ - Example: Tab 0 = Seek (job search), Tab 1 = Immigration NZ (employer check), Tab 2 = LinkedIn (research)
156
+
150
157
  Workflow for form filling (e.g. "注册一个 Gmail 账号"):
151
158
  1. browser_connect + browser_navigate → go to the form page
152
159
  2. browser_snapshot → see all form fields with ref numbers
@@ -163,6 +170,21 @@ Workflow for form filling (e.g. "注册一个 Gmail 账号"):
163
170
  - Browser approach too slow → consider building a script/CLI solution
164
171
  - One data source fails → try an alternative source
165
172
  - If stuck after 2 failed attempts at the same step, try a fundamentally different approach
173
+ - **Popup/overlay blocks interaction** → The system auto-tries Escape + close buttons. If that fails, navigate directly to the target URL (e.g. append search params to skip the homepage popup). Do NOT repeatedly try to click through the same overlay.
174
+ - **Login/auth wall** → Use browser_request_user_action to ask the user to log in. Use memory_store to remember which sites require login so you can warn the user upfront next time.
175
+ - **Stale refs after navigation** → Refs are automatically cleared on navigation. Always re-snapshot after browser_navigate, goBack, or reload. If browser_act returns a "hint" about stale refs, re-snapshot immediately.
176
+
177
+ 10. DOCUMENT OUTPUT FORMAT:
178
+ - When creating documents, reports, or deliverables for the user, ALWAYS prefer Microsoft Office formats (.docx for text documents, .xlsx for spreadsheets, .pptx for presentations) over Markdown (.md).
179
+ - Regular users need to open, read, and edit these files — Office formats are universally accessible.
180
+ - Use the Bash tool to install and run document-generation libraries as needed:
181
+ * Python: python-docx (Word), openpyxl (Excel), python-pptx (PowerPoint)
182
+ * Node.js: docx (Word), exceljs (Excel), pptx (PowerPoint)
183
+ - Only use Markdown when:
184
+ * The output is internal (e.g. README.md in a program directory, skill instructions)
185
+ * The user explicitly requests Markdown
186
+ * The content is a quick inline response in the conversation (not a standalone file)
187
+ - When writing .docx files, include proper formatting: headings, paragraphs, tables, bullet lists — not just plain text dumped into a Word file.
166
188
 
167
189
  Guidelines:
168
190
  - SELF-VERIFY before finishing: re-read modified files, take a final screenshot after browser actions, or re-check output to confirm correctness. Never assume success without confirming the end state.
@@ -199,6 +199,9 @@ export class BrowserController {
199
199
 
200
200
  async navigate(url: string): Promise<string> {
201
201
  this.ensureConnected();
202
+ // Clear refs from previous page — they become stale after navigation
203
+ this.refCache.clear();
204
+ this.frameContexts.clear();
202
205
  await this.send("Page.navigate", { url });
203
206
  // Wait for load
204
207
  await this.waitForLoad();
@@ -208,6 +211,8 @@ export class BrowserController {
208
211
 
209
212
  async goBack(): Promise<string> {
210
213
  this.ensureConnected();
214
+ this.refCache.clear();
215
+ this.frameContexts.clear();
211
216
  try {
212
217
  // Get navigation history and go to the previous entry
213
218
  const history = (await this.send("Page.getNavigationHistory")) as {
@@ -235,6 +240,8 @@ export class BrowserController {
235
240
 
236
241
  async reload(): Promise<string> {
237
242
  this.ensureConnected();
243
+ this.refCache.clear();
244
+ this.frameContexts.clear();
238
245
  await this.send("Page.reload");
239
246
  await this.waitForLoad();
240
247
  return "Page reloaded.";
@@ -1278,6 +1285,52 @@ export class BrowserController {
1278
1285
  }
1279
1286
  }
1280
1287
 
1288
+ // ── Overlay Dismissal ────────────────────────────────────────────
1289
+
1290
+ /**
1291
+ * Attempt to dismiss popups/overlays blocking interaction.
1292
+ * Tries Escape key first, then common close button patterns.
1293
+ * Called automatically when clickRef detects an element is covered.
1294
+ */
1295
+ private async tryDismissOverlay(): Promise<boolean> {
1296
+ try {
1297
+ // Strategy 1: Press Escape to dismiss modals/popups
1298
+ await this.pressKey("Escape");
1299
+ await new Promise((r) => setTimeout(r, 300));
1300
+
1301
+ // Strategy 2: Click common close/dismiss buttons
1302
+ await this.send("Runtime.evaluate", {
1303
+ expression: `
1304
+ (function() {
1305
+ var selectors = [
1306
+ 'button[aria-label="Close"]', 'button[aria-label="close"]',
1307
+ 'button[aria-label="Dismiss"]', 'button[aria-label="dismiss"]',
1308
+ '[role="dialog"] button[aria-label*="close" i]',
1309
+ '[role="dialog"] button[aria-label*="dismiss" i]',
1310
+ '[data-dismiss]', '[data-close]',
1311
+ '.modal-close', '[class*="close-button"]',
1312
+ '[class*="CloseButton"]', '[class*="dismiss"]',
1313
+ ];
1314
+ for (var i = 0; i < selectors.length; i++) {
1315
+ try {
1316
+ var btn = document.querySelector(selectors[i]);
1317
+ if (btn && btn.offsetParent !== null) {
1318
+ btn.click();
1319
+ return 'clicked';
1320
+ }
1321
+ } catch(e) {}
1322
+ }
1323
+ return 'none';
1324
+ })()
1325
+ `,
1326
+ returnByValue: true,
1327
+ });
1328
+ return true;
1329
+ } catch {
1330
+ return false;
1331
+ }
1332
+ }
1333
+
1281
1334
  // ── Ref-based Interactions (CDP Input Events) ─────────────────────
1282
1335
 
1283
1336
  /**
@@ -1289,6 +1342,8 @@ export class BrowserController {
1289
1342
  * Includes auto-wait: retries up to 3 times (with 500ms intervals) if the
1290
1343
  * element is not yet actionable (e.g., covered by a loading overlay, still
1291
1344
  * animating into view). This matches Playwright's auto-waiting behavior.
1345
+ * When an element is covered by an overlay, auto-dismiss is attempted
1346
+ * (Escape key + common close buttons) before retrying.
1292
1347
  */
1293
1348
  async clickRef(refId: number): Promise<RefActionResult> {
1294
1349
  this.ensureConnected();
@@ -1311,7 +1366,13 @@ export class BrowserController {
1311
1366
 
1312
1367
  if (resolved.error) {
1313
1368
  lastError = resolved.error;
1314
- // If element is covered or hidden, wait and retry (it might be animating)
1369
+ // If element is covered by an overlay, try auto-dismissing it before retry
1370
+ if (resolved.error.includes("Element is covered by") && attempt < maxRetries - 1) {
1371
+ await this.tryDismissOverlay();
1372
+ await new Promise((r) => setTimeout(r, 500));
1373
+ continue;
1374
+ }
1375
+ // If element is hidden or other issue, wait and retry (it might be animating)
1315
1376
  if (attempt < maxRetries - 1) {
1316
1377
  await new Promise((r) => setTimeout(r, 500));
1317
1378
  continue;
@@ -1528,8 +1589,17 @@ export class BrowserController {
1528
1589
  success,
1529
1590
  });
1530
1591
 
1531
- // If an action failed, stop the batch (remaining refs may be stale)
1532
- if (!success) break;
1592
+ // If an action failed, stop the batch (remaining refs may be stale).
1593
+ // Append re-snapshot hint to help the agent recover.
1594
+ if (!success) {
1595
+ results.push({
1596
+ action: "hint",
1597
+ result:
1598
+ "Refs may be stale after failure. Use browser_snapshot to get fresh refs before retrying.",
1599
+ success: true,
1600
+ });
1601
+ break;
1602
+ }
1533
1603
 
1534
1604
  // Brief pause between actions for DOM to settle
1535
1605
  if (spec.action !== "wait") {
package/src/db/types.ts CHANGED
@@ -49,6 +49,7 @@ export type EventType =
49
49
  | "tool_use_input"
50
50
  | "tool_result"
51
51
  | "tool_failure"
52
+ | "tool_failure_hint"
52
53
  | "thinking"
53
54
  | "error"
54
55
  | "status_change"
@@ -92,7 +92,11 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
92
92
  {
93
93
  name: z.string().describe("Skill name in kebab-case, e.g. 'flight-booking'"),
94
94
  description: z.string().describe("One-line description of what this skill does"),
95
- instructions: z.string().describe("Markdown step-by-step instructions"),
95
+ instructions: z.string().describe(
96
+ "Markdown step-by-step instructions. " +
97
+ "IMPORTANT: When a skill produces document output (reports, summaries, etc.), " +
98
+ "instruct it to generate .docx/.xlsx/.pptx files (not .md), so end users can open and edit them."
99
+ ),
96
100
  emoji: z.string().optional().describe("Single emoji representing this skill"),
97
101
  },
98
102
  async (args) => {
@@ -400,10 +404,21 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
400
404
  response += `**Existing skills (do NOT duplicate):** ${existingNames.join(", ")}\n\n`;
401
405
  }
402
406
 
403
- response += `**Your task:** Analyze this job description and decompose it into 4-10 automatable skills.\n\n`;
404
- response += `**IMPORTANT You MUST use ask_user before creating skills:**\n`;
405
- response += `1. Analyze the job and draft a list of proposed skills (name, emoji, one-line description for each).\n`;
406
- response += `2. Call \`ask_user\` with the formatted skill list as "question" and these options:\n`;
407
+ response += `**Your task:** Analyze this job description and do TWO things:\n`;
408
+ response += `- **A. Rewrite the description into a professional, structured prompt** that clearly defines the job's objectives, scope, and expected outcomes. This will be used as the actual execution prompt when running the job. Store it as the \`prompt\` parameter when calling \`skill_link_job\`.\n`;
409
+ response += `- **B. Decompose the job into 4-10 automatable skills.**\n\n`;
410
+ response += `**Example Task A (professional prompt rewrite):**\n`;
411
+ response += `> User description: "我是电商运营,每天看竞品价格、写文案、回复评论"\n`;
412
+ response += `> Professional prompt:\n`;
413
+ response += `> 作为电商运营专员,系统性地执行以下核心日常职责:\n`;
414
+ response += `> 1. 竞品价格监控 — 采集主要竞品在各电商平台的实时价格,识别价格变动趋势,生成对比报告(.docx)\n`;
415
+ response += `> 2. 商品文案撰写 — 根据产品卖点、目标受众和平台调性,撰写符合 SEO 规范的商品标题与描述\n`;
416
+ response += `> 3. 客户评论管理 — 检查新增差评与咨询,按优先级分类,起草专业回复\n`;
417
+ response += `> 完成后汇总当日工作摘要,标注需要人工关注的事项。\n\n`;
418
+ response += `The prompt should be in the same language as the user's description. Keep it under 2000 characters.\n\n`;
419
+ response += `**Workflow (follow these steps in order):**\n`;
420
+ response += `1. Draft the professional prompt (task A) AND a list of proposed skills with name, emoji, one-line description each (task B).\n`;
421
+ response += `2. Call \`ask_user\` with both the rewritten prompt and the skill list as "question", and these options:\n`;
407
422
  response += ` - options: [{label: "Approve All", action_key: "approve_all", description: "Create all proposed skills"}, {label: "Cancel", action_key: "cancel", description: "Do not create any skills"}]\n`;
408
423
  response += `3. WAIT for the response. If action_key is "approve_all", create all skills using \`skill_create\`. If "cancel", stop.\n`;
409
424
  response += `4. Do NOT ask for confirmation in text. Do NOT create skills without calling ask_user first.\n\n`;
@@ -413,13 +428,14 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
413
428
  response += `- instructions: detailed step-by-step markdown instructions the agent can follow\n`;
414
429
  response += `- emoji: a single emoji representing the skill\n\n`;
415
430
  response += `skill_create automatically adds the skill to the user's collection — no need to call skill_add.\n\n`;
416
- response += `After ALL skills are created, call \`skill_link_job\` with job_name="${args.job_name}" and the list of created skill names to link them and mark the job as analyzed.\n\n`;
431
+ response += `After ALL skills are created, call \`skill_link_job\` with job_name="${args.job_name}", the list of created skill names, AND the professional prompt you drafted to link them and mark the job as analyzed.\n\n`;
417
432
  response += `**Guidelines for skill instructions:**\n`;
418
433
  response += `- Write clear, actionable markdown steps\n`;
419
434
  response += `- Reference browser tools (browser_navigate, browser_click, browser_read_page, etc.) for web tasks\n`;
420
435
  response += `- Include error handling steps\n`;
421
436
  response += `- Use placeholders like {query}, {date} for variable inputs\n`;
422
437
  response += `- Each skill should be a single, well-defined workflow (10-25 steps)\n`;
438
+ response += `- When a skill produces document output (reports, summaries, data exports), instruct it to generate .docx/.xlsx/.pptx files — NOT .md — so end users can open and edit them\n`;
423
439
 
424
440
  return { content: [{ type: "text", text: response }] };
425
441
  }
@@ -431,10 +447,15 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
431
447
  job_name: z.string().describe("Name of the job to link skills to"),
432
448
  job_description: z.string().describe("Job description (used if job doesn't exist yet)"),
433
449
  skill_names: z.array(z.string()).describe("Names of skills to link to this job"),
450
+ prompt: z.string().optional().describe(
451
+ "Professional, structured rewrite of the job description (max 2000 chars). " +
452
+ "This becomes the actual execution prompt when running the job. " +
453
+ "Should clearly define objectives, scope, and expected outcomes."
454
+ ),
434
455
  },
435
456
  async (args) => {
436
457
  try {
437
- await saveJobToDb(args.job_name, args.job_description, args.skill_names);
458
+ await saveJobToDb(args.job_name, args.job_description, args.skill_names, args.prompt);
438
459
  log.success(
439
460
  `Job "${args.job_name}": linked ${args.skill_names.length} skills and marked as analyzed`
440
461
  );
@@ -1393,13 +1414,15 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
1393
1414
  async function saveJobToDb(
1394
1415
  jobName: string,
1395
1416
  jobDescription: string,
1396
- createdSkillNames: string[]
1417
+ createdSkillNames: string[],
1418
+ prompt?: string
1397
1419
  ): Promise<void> {
1398
1420
  try {
1399
1421
  const data = await callMcpHandler("job.save_with_skills", {
1400
1422
  job_name: jobName,
1401
1423
  job_description: jobDescription,
1402
1424
  skill_names: createdSkillNames,
1425
+ prompt: prompt || null,
1403
1426
  });
1404
1427
 
1405
1428
  log.debug(
@@ -226,13 +226,14 @@ export class Orchestrator {
226
226
  await this.dispatchAndWait(`[JobRun: ${jobRun.job_name}] ${prompt}`);
227
227
  await runner.completeRun(jobRun.id, "completed", "Job executed via web trigger");
228
228
  } catch (err) {
229
- log.error(`Job run failed: ${err}`);
229
+ const errMsg = err instanceof Error ? err.message : String(err);
230
+ log.error(`Job run failed: ${errMsg}`);
231
+ // Preserve partial progress info in the summary
232
+ const summary = errMsg.includes("cancelled")
233
+ ? `Cancelled by user. Check workspace for any partial results.`
234
+ : `Execution error: ${errMsg}`;
230
235
  await runner
231
- .completeRun(
232
- jobRun.id,
233
- "failed",
234
- `Execution error: ${err instanceof Error ? err.message : err}`
235
- )
236
+ .completeRun(jobRun.id, "failed", summary)
236
237
  .catch((e) => log.error(`Failed to mark run as failed: ${e}`));
237
238
  }
238
239
  }
@@ -29,7 +29,7 @@ const CONFIG_DEFAULTS: Partial<AssistMeConfig> = {
29
29
  supabaseAnonKey: SUPABASE_ANON_KEY_DEFAULT,
30
30
  sessionName: "Default",
31
31
  model: "claude-sonnet-4-20250514",
32
- taskTimeoutMinutes: 10,
32
+ taskTimeoutMinutes: 0,
33
33
  };
34
34
 
35
35
  const config = new Conf<Partial<AssistMeConfig>>({
@@ -51,7 +51,7 @@ export function getConfig(): AssistMeConfig {
51
51
  workspacePath: resolve(workspacePath),
52
52
  sessionName: config.get("sessionName") || "Default",
53
53
  model: config.get("model") || "claude-sonnet-4-20250514",
54
- taskTimeoutMinutes: config.get("taskTimeoutMinutes") || 10,
54
+ taskTimeoutMinutes: config.get("taskTimeoutMinutes") ?? 0,
55
55
  };
56
56
  }
57
57
 
@@ -53,6 +53,7 @@ export const JobRowSchema = z.object({
53
53
  job_id: z.string(),
54
54
  job_name: z.string(),
55
55
  job_description: z.string().optional().default(""),
56
+ job_prompt: z.string().optional().nullable(),
56
57
  skill_id: z.string().optional().nullable(),
57
58
  skill_name: z.string().optional().nullable(),
58
59
  skill_description: z.string().optional().default(""),
@@ -64,6 +65,7 @@ export const JobListRowSchema = z.object({
64
65
  id: z.string(),
65
66
  name: z.string(),
66
67
  description: z.string().optional().default(""),
68
+ prompt: z.string().optional().nullable(),
67
69
  skill_count: z.number().optional().default(0),
68
70
  });
69
71
 
@@ -206,6 +206,7 @@ describe("createEventHooks", () => {
206
206
  name: "Bash",
207
207
  error: "command not found: bad-cmd",
208
208
  failure_count: 1,
209
+ browser_failure_count: 0,
209
210
  });
210
211
  });
211
212
 
@@ -39,6 +39,7 @@ vi.mock("../../src/utils/config.js", () => ({
39
39
  workspacePath: "/tmp/test-workspace",
40
40
  maxTurns: 10,
41
41
  anthropicApiKey: "sk-ant-test",
42
+ taskTimeoutMinutes: 0,
42
43
  }),
43
44
  getAssistMeRoot: () => "/tmp/test-workspace/assistme",
44
45
  }));
@@ -78,6 +79,7 @@ vi.mock("../../src/agent/memory.js", () => ({
78
79
  MemoryManager: class MockMemoryManager {
79
80
  buildMemoryPrompt = vi.fn().mockResolvedValue("");
80
81
  remember = mockRemember;
82
+ compressIfNeeded = vi.fn().mockResolvedValue(undefined);
81
83
  },
82
84
  }));
83
85
 
@@ -85,6 +87,7 @@ vi.mock("../../src/agent/skills.js", () => ({
85
87
  SkillManager: class MockSkillManager {
86
88
  setUserId = vi.fn();
87
89
  loadFromDb = vi.fn().mockResolvedValue(undefined);
90
+ ensureLoaded = vi.fn().mockResolvedValue(undefined);
88
91
  buildSkillPrompt = vi.fn().mockReturnValue("");
89
92
  buildSkillDescriptions = vi.fn().mockResolvedValue("");
90
93
  findRelevant = vi.fn().mockReturnValue([]);
@@ -136,6 +139,14 @@ vi.mock("../../src/agent/self-analyzer.js", () => ({
136
139
  analyzeSelfPostTask: vi.fn().mockResolvedValue(undefined),
137
140
  }));
138
141
 
142
+ vi.mock("../../src/agent/prompt-builder.js", () => ({
143
+ buildSystemPrompt: vi.fn().mockResolvedValue("mock system prompt"),
144
+ }));
145
+
146
+ vi.mock("../../src/agent/skill-evaluator.js", () => ({
147
+ evaluateAndMaybeCreateSkill: vi.fn().mockResolvedValue(undefined),
148
+ }));
149
+
139
150
  const { TaskProcessor } = await import("../../src/agent/processor.js");
140
151
 
141
152
  describe("TaskProcessor", () => {
@@ -50,7 +50,7 @@ describe("config module", () => {
50
50
  const config = getConfig();
51
51
  expect(config.sessionName).toBe("Default");
52
52
  expect(config.model).toBe("claude-sonnet-4-20250514");
53
- expect(config.taskTimeoutMinutes).toBe(10);
53
+ expect(config.taskTimeoutMinutes).toBe(0);
54
54
  });
55
55
 
56
56
  it("resolves workspacePath to absolute path", () => {
@@ -1,7 +0,0 @@
1
- import {
2
- JobRunner
3
- } from "./chunk-QGH5MFJA.js";
4
- import "./chunk-HY3FFXSQ.js";
5
- export {
6
- JobRunner
7
- };