openclaw-telegram-manager 2.3.2 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +74 -58
  2. package/dist/commands/autopilot.d.ts.map +1 -1
  3. package/dist/commands/autopilot.js +9 -6
  4. package/dist/commands/autopilot.js.map +1 -1
  5. package/dist/commands/daily-report.d.ts.map +1 -1
  6. package/dist/commands/daily-report.js +11 -10
  7. package/dist/commands/daily-report.js.map +1 -1
  8. package/dist/commands/doctor-all.d.ts.map +1 -1
  9. package/dist/commands/doctor-all.js +6 -9
  10. package/dist/commands/doctor-all.js.map +1 -1
  11. package/dist/commands/doctor.d.ts.map +1 -1
  12. package/dist/commands/doctor.js +3 -11
  13. package/dist/commands/doctor.js.map +1 -1
  14. package/dist/commands/init.d.ts.map +1 -1
  15. package/dist/commands/init.js +22 -32
  16. package/dist/commands/init.js.map +1 -1
  17. package/dist/commands/rename.d.ts.map +1 -1
  18. package/dist/commands/rename.js +2 -4
  19. package/dist/commands/rename.js.map +1 -1
  20. package/dist/commands/snooze.js +2 -2
  21. package/dist/commands/snooze.js.map +1 -1
  22. package/dist/commands/status.d.ts.map +1 -1
  23. package/dist/commands/status.js +74 -6
  24. package/dist/commands/status.js.map +1 -1
  25. package/dist/commands/sync.js +1 -1
  26. package/dist/commands/sync.js.map +1 -1
  27. package/dist/commands/upgrade.js +2 -2
  28. package/dist/commands/upgrade.js.map +1 -1
  29. package/dist/lib/capsule.js +3 -3
  30. package/dist/lib/capsule.js.map +1 -1
  31. package/dist/lib/doctor-checks.d.ts.map +1 -1
  32. package/dist/lib/doctor-checks.js +26 -59
  33. package/dist/lib/doctor-checks.js.map +1 -1
  34. package/dist/lib/include-generator.d.ts +1 -1
  35. package/dist/lib/include-generator.js +4 -4
  36. package/dist/lib/include-generator.js.map +1 -1
  37. package/dist/lib/registry.d.ts.map +1 -1
  38. package/dist/lib/registry.js +3 -0
  39. package/dist/lib/registry.js.map +1 -1
  40. package/dist/lib/telegram.d.ts +9 -4
  41. package/dist/lib/telegram.d.ts.map +1 -1
  42. package/dist/lib/telegram.js +87 -76
  43. package/dist/lib/telegram.js.map +1 -1
  44. package/dist/lib/types.d.ts +2 -2
  45. package/dist/lib/types.js +1 -1
  46. package/dist/lib/types.js.map +1 -1
  47. package/dist/plugin.js +288 -291
  48. package/dist/setup.js +3 -3
  49. package/dist/setup.js.map +1 -1
  50. package/dist/tool.js +0 -18
  51. package/dist/tool.js.map +1 -1
  52. package/package.json +10 -1
  53. package/skills/tm/SKILL.md +4 -4
package/dist/plugin.js CHANGED
@@ -8850,9 +8850,9 @@ var TopicEntrySchema = Type.Object({
8850
8850
  lastMessageAt: Type.Union([Type.String(), Type.Null()]),
8851
8851
  lastDoctorReportAt: Type.Union([Type.String(), Type.Null()]),
8852
8852
  lastDoctorRunAt: Type.Union([Type.String(), Type.Null()]),
8853
+ lastDailyReportAt: Type.Union([Type.String(), Type.Null()]),
8853
8854
  lastCapsuleWriteAt: Type.Union([Type.String(), Type.Null()]),
8854
8855
  snoozeUntil: Type.Union([Type.String(), Type.Null()]),
8855
- ignoreChecks: Type.Array(Type.String()),
8856
8856
  consecutiveSilentDoctors: Type.Integer({ minimum: 0 }),
8857
8857
  lastPostError: Type.Union([Type.String({ maxLength: MAX_POST_ERROR_LENGTH }), Type.Null()]),
8858
8858
  extras: Type.Record(Type.String(), Type.Unknown())
@@ -8925,6 +8925,9 @@ var migrations = {
8925
8925
  if (entry["lastCapsuleWriteAt"] === void 0) {
8926
8926
  entry["lastCapsuleWriteAt"] = null;
8927
8927
  }
8928
+ if (entry["lastDailyReportAt"] === void 0) {
8929
+ entry["lastDailyReportAt"] = null;
8930
+ }
8928
8931
  }
8929
8932
  }
8930
8933
  return data;
@@ -9128,21 +9131,19 @@ _Describe what this topic is about._
9128
9131
  `,
9129
9132
  "STATUS.md": (name) => `# Status: ${name}
9130
9133
 
9131
- > This file is maintained by the agent \u2014 just send messages in the chat.
9132
-
9133
9134
  ## Last done (UTC)
9134
9135
 
9135
9136
  ${(/* @__PURE__ */ new Date()).toISOString()}
9136
9137
 
9137
- _Waiting for first instructions._
9138
+ Topic created. Waiting for first instructions.
9138
9139
 
9139
9140
  ## Next actions (now)
9140
9141
 
9141
- 1. _e.g. Set up project scaffolding_
9142
+ _None yet._
9142
9143
 
9143
9144
  ## Upcoming actions
9144
9145
 
9145
- _See TODO.md for full backlog._
9146
+ _None yet._
9146
9147
  `,
9147
9148
  "TODO.md": (name) => `# TODO: ${name}
9148
9149
 
@@ -9211,7 +9212,7 @@ function scaffoldCapsule(projectsBase, slug, name, type) {
9211
9212
  throw new Error(`Path escapes projects directory: ${slug}`);
9212
9213
  }
9213
9214
  if (rejectSymlink(projectsBase)) {
9214
- throw new Error(`Projects base is a symlink: ${projectsBase}`);
9215
+ throw new Error("Detected an unsafe file system configuration (symlink)");
9215
9216
  }
9216
9217
  fs4.mkdirSync(capsuleDir, { recursive: false });
9217
9218
  for (const file of BASE_FILES) {
@@ -9239,7 +9240,7 @@ function upgradeCapsule(projectsBase, slug, name, type, currentVersion) {
9239
9240
  throw new Error(`Path escapes projects directory: ${slug}`);
9240
9241
  }
9241
9242
  if (rejectSymlink(capsuleDir)) {
9242
- throw new Error(`Capsule directory is a symlink: ${capsuleDir}`);
9243
+ throw new Error(`Topic directory is a symlink: ${capsuleDir}`);
9243
9244
  }
9244
9245
  const addedFiles = [];
9245
9246
  for (const file of BASE_FILES) {
@@ -9329,27 +9330,39 @@ function checkAuthorization(userId, command, registry, topicAllowFrom) {
9329
9330
 
9330
9331
  // src/lib/telegram.ts
9331
9332
  var TELEGRAM_MSG_LIMIT = 4096;
9332
- function buildDailyReport(data) {
9333
- const n = htmlEscape(data.name);
9333
+ var HEALTH_LABELS = {
9334
+ fresh: "\u2705 Active",
9335
+ // green check
9336
+ stale: "\u23F3 Inactive",
9337
+ // hourglass
9338
+ blocked: "\u26A0\uFE0F Blocked"
9339
+ // warning
9340
+ };
9341
+ function buildDailyReport(data, format = "html") {
9342
+ const isHtml = format === "html";
9343
+ const esc = (s) => isHtml ? htmlEscape(s) : s;
9344
+ const bold = (s) => isHtml ? `<b>${s}</b>` : `**${s}**`;
9345
+ const n = esc(data.name);
9346
+ const healthLabel = HEALTH_LABELS[data.health] ?? data.health;
9334
9347
  const lines = [
9335
- `<b>Daily Report: ${n}</b>`,
9348
+ bold(`Daily Report: ${n}`),
9336
9349
  "",
9337
- `<b>Done today</b>`,
9338
- htmlEscape(data.doneContent),
9350
+ bold("Done today"),
9351
+ esc(data.doneContent),
9339
9352
  "",
9340
- `<b>New learnings</b>`,
9341
- htmlEscape(data.learningsContent),
9353
+ bold("New learnings"),
9354
+ esc(data.learningsContent),
9342
9355
  "",
9343
- `<b>Blockers/Risks</b>`,
9344
- htmlEscape(data.blockersContent),
9356
+ bold("Blockers/Risks"),
9357
+ esc(data.blockersContent),
9345
9358
  "",
9346
- `<b>Next actions (now)</b>`,
9347
- htmlEscape(data.nextContent),
9359
+ bold("Next actions (now)"),
9360
+ esc(data.nextContent),
9348
9361
  "",
9349
- `<b>Upcoming</b>`,
9350
- htmlEscape(data.upcomingContent),
9362
+ bold("Upcoming"),
9363
+ esc(data.upcomingContent),
9351
9364
  "",
9352
- `<b>Health:</b> ${data.health}`
9365
+ `${bold("Health:")} ${healthLabel}`
9353
9366
  ];
9354
9367
  return truncateMessage(lines.join("\n"));
9355
9368
  }
@@ -9364,13 +9377,9 @@ function buildDoctorButtons(groupId, threadId, secret, userId) {
9364
9377
  const cb = (action) => buildCallbackData(action, groupId, threadId, secret, userId);
9365
9378
  return buildInlineKeyboard([
9366
9379
  [
9367
- { text: "Fix", callback_data: cb("fix") },
9368
9380
  { text: "Snooze 7d", callback_data: cb("snooze7d") },
9369
- { text: "Snooze 30d", callback_data: cb("snooze30d") }
9370
- ],
9371
- [
9372
- { text: "Archive", callback_data: cb("archive") },
9373
- { text: "Ignore check", callback_data: cb("ignore") }
9381
+ { text: "Snooze 30d", callback_data: cb("snooze30d") },
9382
+ { text: "Archive topic", callback_data: cb("archive") }
9374
9383
  ]
9375
9384
  ]);
9376
9385
  }
@@ -9397,23 +9406,19 @@ function buildInitConfirmButton(groupId, threadId, secret, userId, type) {
9397
9406
  const cb = buildCallbackData(actionMap[type], groupId, threadId, secret, userId);
9398
9407
  return buildInlineKeyboard([[{ text: "Use this name", callback_data: cb }]]);
9399
9408
  }
9400
- function buildTopicCard(name, slug, type, capsuleVersion) {
9409
+ function buildTopicCard(name, slug, type) {
9401
9410
  return [
9402
9411
  `**Topic: ${name}**`,
9403
- `Type: ${type} | Version: ${capsuleVersion}`,
9404
- `Capsule: projects/${slug}/`,
9412
+ `**Type:** ${type}`,
9413
+ `**Stored in:** projects/${slug}/`,
9405
9414
  "",
9406
9415
  "**How it works**",
9407
- "Just send your instructions in this topic. The agent",
9408
- "maintains STATUS.md and TODO.md automatically as it",
9409
- "works \u2014 nothing is lost on reset or context compaction.",
9410
- "Doctor checks run periodically and alert you if anything",
9411
- "needs attention.",
9416
+ "Talk to the AI in this topic like you normally would \u2014 describe what you need, ask questions, or give instructions. Progress, TODOs, and decisions are tracked automatically so nothing is lost between sessions.",
9412
9417
  "",
9413
- "**Commands:**",
9414
- "/tm status \u2014 quick STATUS.md view",
9418
+ "**Available commands:**",
9419
+ "/tm status \u2014 see current progress",
9415
9420
  "/tm doctor \u2014 run health checks",
9416
- "/tm rename <name> \u2014 rename this topic",
9421
+ "/tm rename new-name \u2014 rename this topic",
9417
9422
  "/tm list \u2014 all topics",
9418
9423
  "/tm archive \u2014 archive this topic",
9419
9424
  "/tm help \u2014 full command reference"
@@ -9423,7 +9428,7 @@ function buildInitWelcomeHtml() {
9423
9428
  return [
9424
9429
  "<b>Set up a new topic workcell</b>",
9425
9430
  "",
9426
- "A workcell gives this topic its own capsule \u2014 a set of markdown files (STATUS.md, TODO.md, etc.) that persist across resets and context compaction.",
9431
+ "A workcell gives this topic a persistent memory \u2014 The AI writes status, TODOs, and notes to disk so context survives between sessions.",
9427
9432
  "",
9428
9433
  "<b>Pick a type:</b>",
9429
9434
  "\u2022 <b>Coding</b> \u2014 adds ARCHITECTURE.md + DEPLOY.md",
@@ -9441,59 +9446,61 @@ function buildInitNameConfirmHtml(name, type) {
9441
9446
  `Name: <b>${n}</b>`,
9442
9447
  `Type: ${t}`,
9443
9448
  "",
9444
- "This name appears in status reports and doctor checks.",
9449
+ "You'll see this name in reports and health checks.",
9445
9450
  "",
9446
9451
  `For a custom name: <code>/tm init your-name ${t}</code>`
9447
9452
  ].join("\n");
9448
9453
  }
9449
- function buildTopicCardHtml(name, slug, type, capsuleVersion) {
9454
+ function buildTopicCardHtml(name, slug, type) {
9450
9455
  const n = htmlEscape(name);
9451
9456
  const s = htmlEscape(slug);
9452
9457
  const t = htmlEscape(type);
9453
9458
  return [
9454
9459
  `<b>Topic: ${n}</b>`,
9455
- `Type: ${t} | Version: ${capsuleVersion}`,
9456
- `Capsule: <code>projects/${s}/</code>`,
9460
+ `<b>Type:</b> ${t}`,
9461
+ `<b>Stored in:</b> <code>projects/${s}/</code>`,
9457
9462
  "",
9458
9463
  "<b>How it works</b>",
9459
- "Just send your instructions in this topic. The agent",
9460
- "maintains STATUS.md and TODO.md automatically as it",
9461
- "works \u2014 nothing is lost on reset or context compaction.",
9462
- "Doctor checks run periodically and alert you if anything",
9463
- "needs attention.",
9464
+ "Talk to the AI in this topic like you normally would \u2014 describe what you need, ask questions, or give instructions. Progress, TODOs, and decisions are tracked automatically so nothing is lost between sessions.",
9464
9465
  "",
9465
- "<b>Commands:</b>",
9466
- "/tm status \u2014 quick STATUS.md view",
9466
+ "<b>Available commands:</b>",
9467
+ "/tm status \u2014 see current progress",
9467
9468
  "/tm doctor \u2014 run health checks",
9468
- "/tm rename &lt;name&gt; \u2014 rename this topic",
9469
+ "/tm rename new-name \u2014 rename this topic",
9469
9470
  "/tm list \u2014 all topics",
9470
9471
  "/tm archive \u2014 archive this topic",
9471
9472
  "/tm help \u2014 full command reference"
9472
9473
  ].join("\n");
9473
9474
  }
9475
+ function formatCommands(text, isHtml) {
9476
+ return text.replace(
9477
+ /\/tm\s\S+(?:\s\S+)*/g,
9478
+ (match) => isHtml ? `<code>${htmlEscape(match)}</code>` : `\`${match}\``
9479
+ );
9480
+ }
9474
9481
  function buildDoctorReport(name, results, format = "markdown") {
9475
9482
  const isHtml = format === "html";
9476
9483
  const n = isHtml ? htmlEscape(name) : name;
9477
9484
  const bold = (s) => isHtml ? `<b>${s}</b>` : `**${s}**`;
9478
- const code = (s) => isHtml ? `<code>${s}</code>` : `\`${s}\``;
9479
- const lines = [bold(`Doctor: ${n}`), ""];
9480
- if (results.length === 0) {
9481
- lines.push("All checks passed.");
9485
+ const lines = [bold(`Health check: ${n}`), ""];
9486
+ const significant = results.filter((r) => r.severity !== Severity.INFO);
9487
+ if (significant.length === 0) {
9488
+ lines.push("All good \u2014 no issues found.");
9482
9489
  return lines.join("\n");
9483
9490
  }
9484
- for (const r of results) {
9491
+ for (let i = 0; i < significant.length; i++) {
9492
+ const r = significant[i];
9485
9493
  const icon = severityIcon(r.severity);
9486
9494
  const msg = isHtml ? htmlEscape(r.message) : r.message;
9487
- const checkId = isHtml ? htmlEscape(r.checkId) : r.checkId;
9488
- const fix = r.fixable ? " [fixable]" : "";
9489
- lines.push(`${icon} ${code(checkId)}: ${msg}${fix}`);
9495
+ lines.push(`${icon} ${msg}`);
9490
9496
  if (r.remediation) {
9491
- const rem = isHtml ? htmlEscape(r.remediation) : r.remediation;
9492
- lines.push(` \u2192 ${rem}`);
9497
+ const rem = formatCommands(r.remediation, isHtml);
9498
+ lines.push(` \u2192 ${rem}`);
9499
+ }
9500
+ if (i < significant.length - 1) {
9501
+ lines.push("");
9493
9502
  }
9494
9503
  }
9495
- lines.push("");
9496
- lines.push("Reply /tm doctor to re-check, or use the buttons below.");
9497
9504
  return truncateMessage(lines.join("\n"));
9498
9505
  }
9499
9506
  function severityIcon(severity) {
@@ -9515,38 +9522,44 @@ function buildHelpCard() {
9515
9522
  return [
9516
9523
  "**Topic Manager Commands**",
9517
9524
  "",
9518
- "/tm init \u2014 register this topic",
9519
- "/tm status \u2014 quick STATUS.md view",
9520
- "/tm doctor \u2014 run health checks",
9521
- "/tm doctor --all \u2014 check all topics",
9522
- "/tm rename <name> \u2014 rename this topic",
9525
+ "**Basics**",
9526
+ "/tm init \u2014 set up this topic",
9527
+ "/tm status \u2014 see current progress",
9523
9528
  "/tm list \u2014 all topics",
9524
- "/tm sync \u2014 re-apply config",
9525
- "/tm upgrade \u2014 update capsule template",
9526
- "/tm snooze <Nd> \u2014 snooze doctor (7d, 30d, etc.)",
9527
- "/tm archive \u2014 archive topic",
9528
- "/tm unarchive \u2014 reactivate topic",
9529
- "/tm autopilot [enable|disable|status] \u2014 daily sweeps",
9530
- "/tm daily-report \u2014 generate daily status report",
9531
- "/tm help \u2014 this message"
9529
+ "/tm help \u2014 this message",
9530
+ "",
9531
+ "**Health & reports**",
9532
+ "/tm doctor \u2014 run health checks",
9533
+ "/tm doctor --all \u2014 check all topics at once",
9534
+ "/tm daily-report \u2014 post a daily summary",
9535
+ "/tm autopilot enable \u2014 automatic daily health checks",
9536
+ "/tm autopilot disable \u2014 turn off automatic checks",
9537
+ "",
9538
+ "**Manage topics**",
9539
+ "/tm rename new-name \u2014 rename this topic",
9540
+ "/tm snooze 7d \u2014 pause health checks (e.g. 7d, 30d)",
9541
+ "/tm archive \u2014 archive this topic",
9542
+ "/tm unarchive \u2014 bring back an archived topic",
9543
+ "/tm sync \u2014 fix config if out of sync",
9544
+ "/tm upgrade \u2014 update topic files to latest version"
9532
9545
  ].join("\n");
9533
9546
  }
9534
9547
  function buildListMessage(topics) {
9535
9548
  if (topics.length === 0) {
9536
- return "**Topic Registry** (0 topics)\n\nNo topics registered.";
9549
+ return "**Your topics**\n\nNo topics yet. Type /tm init in any topic to get started.";
9537
9550
  }
9538
9551
  const sorted = [...topics].sort((a, b) => {
9539
9552
  const order = { active: 0, snoozed: 1, archived: 2 };
9540
9553
  return (order[a.status] ?? 3) - (order[b.status] ?? 3);
9541
9554
  });
9542
- const lines = [`**Topic Registry** (${topics.length} topics)`, ""];
9555
+ const count = topics.length;
9556
+ const lines = [`**Your topics** (${count})`, ""];
9543
9557
  let rendered = 0;
9544
9558
  for (const t of sorted) {
9545
- const entry = [
9546
- `**${t.name}** [${t.type}] ${t.status}`,
9547
- ` Last active: ${t.lastMessageAt ? relativeTime(t.lastMessageAt) : "never"}`,
9548
- ` Thread: #${t.threadId}`
9549
- ].join("\n");
9559
+ const activity = t.lastMessageAt ? relativeTime(t.lastMessageAt) : "no activity yet";
9560
+ const statusTag = t.status !== "active" ? ` \u2014 ${t.status}` : "";
9561
+ const entry = `**${t.name}** \xB7 ${t.type}${statusTag}
9562
+ ${activity}`;
9550
9563
  const tentative = [...lines, entry, ""].join("\n");
9551
9564
  if (tentative.length > TELEGRAM_MSG_LIMIT - 40) {
9552
9565
  const remaining = sorted.length - rendered;
@@ -9629,7 +9642,7 @@ function getSystemPromptTemplate(name, slug, absoluteWorkspacePath) {
9629
9642
  return `You are the assistant for the Telegram topic: ${name}.
9630
9643
 
9631
9644
  Determinism rules:
9632
- - Source of truth is the project capsule at: ${absoluteWorkspacePath}/projects/${slug}/
9645
+ - Source of truth is the project folder at: ${absoluteWorkspacePath}/projects/${slug}/
9633
9646
  - After /reset, /new, or context compaction: ALWAYS re-read STATUS.md,
9634
9647
  then TODO.md, then LEARNINGS.md (last 20 entries), then COMMANDS.md
9635
9648
  before continuing work. Do not rely on summarized memory for paths,
@@ -9657,10 +9670,10 @@ Learning capture:
9657
9670
 
9658
9671
  Separation:
9659
9672
  - Your workspace is strictly projects/${slug}/. Do not read, write, or reference
9660
- files in any other topic's capsule directory.
9673
+ files in any other topic's project directory.
9661
9674
  - If the user mentions another topic by name or slug, ask for explicit
9662
9675
  confirmation before mixing work: "This references topic X \u2014 switch context?"
9663
- - Never copy data between topic capsules without explicit user instruction.
9676
+ - Never copy data between topic folders without explicit user instruction.
9664
9677
  - Ask one clarifying question if the next action is ambiguous.`;
9665
9678
  }
9666
9679
  function computeRegistryHash(topics) {
@@ -9881,10 +9894,10 @@ async function handleInit(ctx, args) {
9881
9894
  return { text: "Missing context: groupId, threadId, or userId not available. Run this command inside a Telegram forum topic." };
9882
9895
  }
9883
9896
  if (!validateGroupId(groupId)) {
9884
- return { text: "Invalid groupId format." };
9897
+ return { text: "Something went wrong \u2014 this doesn't look like a valid forum topic." };
9885
9898
  }
9886
9899
  if (!validateThreadId(threadId)) {
9887
- return { text: "Invalid threadId format." };
9900
+ return { text: "Something went wrong \u2014 this doesn't look like a valid forum topic." };
9888
9901
  }
9889
9902
  const registry = readRegistry(workspaceDir);
9890
9903
  const auth = checkAuthorization(userId, "init", registry);
@@ -9917,17 +9930,17 @@ async function handleInit(ctx, args) {
9917
9930
  const finalSlug = generateSlug(threadId, groupId, existingSlugs);
9918
9931
  const projectsBase = path6.join(workspaceDir, "projects");
9919
9932
  if (!jailCheck(projectsBase, finalSlug)) {
9920
- return { text: "Path safety check failed. Slug may escape the projects directory." };
9933
+ return { text: "Setup failed \u2014 internal path validation error. Please try again." };
9921
9934
  }
9922
9935
  if (rejectSymlink(projectsBase)) {
9923
- return { text: "Projects base is a symlink. Aborting for security." };
9936
+ return { text: "Setup failed \u2014 detected an unsafe file system configuration." };
9924
9937
  }
9925
9938
  if (fs6.existsSync(path6.join(projectsBase, finalSlug))) {
9926
- return { text: `Directory projects/${finalSlug}/ already exists on disk.` };
9939
+ return { text: "A folder for this topic already exists. Run /tm doctor to investigate." };
9927
9940
  }
9928
9941
  const targetPath = path6.join(projectsBase, finalSlug);
9929
9942
  if (rejectSymlink(targetPath)) {
9930
- return { text: "Target path is a symlink. Aborting for security." };
9943
+ return { text: "Setup failed \u2014 detected an unsafe file system configuration." };
9931
9944
  }
9932
9945
  const isFirstUser = registry.topicManagerAdmins.length === 0;
9933
9946
  try {
@@ -9944,9 +9957,9 @@ async function handleInit(ctx, args) {
9944
9957
  lastMessageAt: (/* @__PURE__ */ new Date()).toISOString(),
9945
9958
  lastDoctorReportAt: null,
9946
9959
  lastDoctorRunAt: null,
9960
+ lastDailyReportAt: null,
9947
9961
  lastCapsuleWriteAt: null,
9948
9962
  snoozeUntil: null,
9949
- ignoreChecks: [],
9950
9963
  consecutiveSilentDoctors: 0,
9951
9964
  lastPostError: null,
9952
9965
  extras: {}
@@ -9973,35 +9986,24 @@ async function handleInit(ctx, args) {
9973
9986
  } catch (err) {
9974
9987
  const msg = err instanceof Error ? err.message : String(err);
9975
9988
  restartMsg = `
9976
- Warning: include generation failed: ${msg}`;
9989
+ Warning: config sync failed: ${msg}`;
9977
9990
  }
9978
9991
  }
9979
9992
  appendAudit(
9980
9993
  workspaceDir,
9981
9994
  buildAuditEntry(userId, "init", finalSlug, `Initialized topic name="${name}" type=${topicType} group=${groupId} thread=${threadId}`)
9982
9995
  );
9983
- const topicCard = buildTopicCard(name, finalSlug, topicType, CAPSULE_VERSION);
9984
- let adminNote = "";
9985
- let adminNoteHtml = "";
9986
- if (isFirstUser) {
9987
- adminNote = "\n\nYou are the first user and have been added as a telegram-manager admin.";
9988
- adminNoteHtml = "\n\nYou are the first user and have been added as a telegram-manager admin.";
9989
- }
9990
- const autopilotTip = "\n\nTip: Enable daily health sweeps with /tm autopilot enable";
9991
- const autopilotTipHtml = "\n\nTip: Enable daily health sweeps with <code>/tm autopilot enable</code>";
9996
+ const topicCard = buildTopicCard(name, finalSlug, topicType);
9992
9997
  if (ctx.postFn && groupId && threadId) {
9993
9998
  try {
9994
- const htmlCard = buildTopicCardHtml(name, finalSlug, topicType, CAPSULE_VERSION);
9995
- await ctx.postFn(groupId, threadId, `${htmlCard}${adminNoteHtml}${autopilotTipHtml}`);
9996
- return {
9997
- text: `Topic "${name}" initialized as ${topicType}. Capsule: projects/${finalSlug}/`,
9998
- pin: true
9999
- };
9999
+ const htmlCard = buildTopicCardHtml(name, finalSlug, topicType);
10000
+ await ctx.postFn(groupId, threadId, htmlCard);
10001
+ return { text: "", pin: true };
10000
10002
  } catch {
10001
10003
  }
10002
10004
  }
10003
10005
  return {
10004
- text: `${topicCard}${adminNote}${restartMsg}${autopilotTip}`,
10006
+ text: `${topicCard}${restartMsg}`,
10005
10007
  pin: true
10006
10008
  };
10007
10009
  }
@@ -10017,10 +10019,10 @@ async function buildTypePicker(ctx) {
10017
10019
  return { text: "Missing context: groupId, threadId, or userId not available. Run this command inside a Telegram forum topic." };
10018
10020
  }
10019
10021
  if (!validateGroupId(groupId)) {
10020
- return { text: "Invalid groupId format." };
10022
+ return { text: "Something went wrong \u2014 this doesn't look like a valid forum topic." };
10021
10023
  }
10022
10024
  if (!validateThreadId(threadId)) {
10023
- return { text: "Invalid threadId format." };
10025
+ return { text: "Something went wrong \u2014 this doesn't look like a valid forum topic." };
10024
10026
  }
10025
10027
  const registry = readRegistry(workspaceDir);
10026
10028
  const auth = checkAuthorization(userId, "init", registry);
@@ -10056,10 +10058,10 @@ async function handleInitTypeSelect(ctx, type) {
10056
10058
  return { text: "Missing context: groupId, threadId, or userId not available. Run this command inside a Telegram forum topic." };
10057
10059
  }
10058
10060
  if (!validateGroupId(groupId)) {
10059
- return { text: "Invalid groupId format." };
10061
+ return { text: "Something went wrong \u2014 this doesn't look like a valid forum topic." };
10060
10062
  }
10061
10063
  if (!validateThreadId(threadId)) {
10062
- return { text: "Invalid threadId format." };
10064
+ return { text: "Something went wrong \u2014 this doesn't look like a valid forum topic." };
10063
10065
  }
10064
10066
  const registry = readRegistry(workspaceDir);
10065
10067
  const auth = checkAuthorization(userId, "init", registry);
@@ -10081,7 +10083,7 @@ async function handleInitTypeSelect(ctx, type) {
10081
10083
  if (ctx.postFn) {
10082
10084
  try {
10083
10085
  await ctx.postFn(groupId, threadId, buildInitNameConfirmHtml(name, type), keyboard);
10084
- return { text: `Type selected: ${type}. Confirm the name or type /tm init <name> ${type}.` };
10086
+ return { text: `Type selected: ${type}. Confirm the name or type /tm init your-name ${type}.` };
10085
10087
  } catch {
10086
10088
  }
10087
10089
  }
@@ -10097,11 +10099,12 @@ function buildInitConfirmMessage(name, type) {
10097
10099
  return [
10098
10100
  "**Almost there!**",
10099
10101
  "",
10100
- `Name: **${name}** | Type: ${type}`,
10102
+ `Name: **${name}**`,
10103
+ `Type: ${type}`,
10101
10104
  "",
10102
- "This name appears in status reports and doctor checks.",
10105
+ "You'll see this name in reports and health checks.",
10103
10106
  "",
10104
- `To use a different name: /tm init <name> ${type}`
10107
+ `For a custom name: \`/tm init your-name ${type}\``
10105
10108
  ].join("\n");
10106
10109
  }
10107
10110
 
@@ -10113,9 +10116,6 @@ import * as path8 from "node:path";
10113
10116
  var import_json52 = __toESM(require_lib(), 1);
10114
10117
  import * as fs7 from "node:fs";
10115
10118
  import * as path7 from "node:path";
10116
- function isIgnored(entry, checkId) {
10117
- return entry.ignoreChecks.includes(checkId);
10118
- }
10119
10119
  function check(severity, checkId, message, fixable, remediation) {
10120
10120
  return remediation ? { severity, checkId, message, fixable, remediation } : { severity, checkId, message, fixable };
10121
10121
  }
@@ -10124,7 +10124,7 @@ function runRegistryChecks(entry, projectsBase) {
10124
10124
  const capsuleDir = path7.join(projectsBase, entry.slug);
10125
10125
  if (!fs7.existsSync(capsuleDir)) {
10126
10126
  results.push(
10127
- check(Severity.ERROR, "pathMissing", `Capsule path does not exist: projects/${entry.slug}/`, false, "Run /tm init to create the capsule, or remove the registry entry")
10127
+ check(Severity.ERROR, "pathMissing", `Project folder is missing (projects/${entry.slug}/)`, false, "Run /tm init to recreate it")
10128
10128
  );
10129
10129
  return results;
10130
10130
  }
@@ -10132,12 +10132,12 @@ function runRegistryChecks(entry, projectsBase) {
10132
10132
  const stat = fs7.statSync(capsuleDir);
10133
10133
  if (!stat.isDirectory()) {
10134
10134
  results.push(
10135
- check(Severity.ERROR, "pathNotDir", `projects/${entry.slug} exists but is not a directory`, false)
10135
+ check(Severity.ERROR, "pathNotDir", "Topic path exists but is not a folder", false)
10136
10136
  );
10137
10137
  }
10138
10138
  } catch {
10139
10139
  results.push(
10140
- check(Severity.ERROR, "pathStatFailed", `Cannot stat projects/${entry.slug}/`, false)
10140
+ check(Severity.ERROR, "pathStatFailed", "Cannot verify topic folder on disk", false)
10141
10141
  );
10142
10142
  }
10143
10143
  return results;
@@ -10148,39 +10148,32 @@ function runCapsuleChecks(entry, projectsBase) {
10148
10148
  if (!fs7.existsSync(capsuleDir)) return results;
10149
10149
  if (!fs7.existsSync(path7.join(capsuleDir, "STATUS.md"))) {
10150
10150
  results.push(
10151
- check(Severity.ERROR, "statusMissing", "STATUS.md is missing from capsule", true, "Run /tm upgrade to recreate STATUS.md, or restore from .tm-backup/STATUS.md if available")
10151
+ check(Severity.ERROR, "statusMissing", "Status file is missing", true, "Run /tm upgrade to recreate it")
10152
10152
  );
10153
10153
  }
10154
10154
  if (!fs7.existsSync(path7.join(capsuleDir, "TODO.md"))) {
10155
- if (!isIgnored(entry, "todoMissing")) {
10156
- results.push(
10157
- check(Severity.WARN, "todoMissing", "TODO.md is missing from capsule", true, "Run /tm upgrade to recreate TODO.md")
10158
- );
10159
- }
10155
+ results.push(
10156
+ check(Severity.WARN, "todoMissing", "TODO file is missing", true, "Run /tm upgrade to recreate it")
10157
+ );
10160
10158
  }
10161
10159
  const overlays = OVERLAY_FILES[entry.type] ?? [];
10162
10160
  for (const file of overlays) {
10163
10161
  if (!fs7.existsSync(path7.join(capsuleDir, file))) {
10164
- const checkId = `overlayMissing:${file}`;
10165
- if (!isIgnored(entry, checkId)) {
10166
- results.push(
10167
- check(Severity.INFO, checkId, `Optional overlay ${file} missing for type "${entry.type}"`, true)
10168
- );
10169
- }
10170
- }
10171
- }
10172
- if (entry.capsuleVersion < CAPSULE_VERSION) {
10173
- if (!isIgnored(entry, "capsuleVersionBehind")) {
10174
10162
  results.push(
10175
- check(
10176
- Severity.INFO,
10177
- "capsuleVersionBehind",
10178
- `Capsule version ${entry.capsuleVersion} is behind current ${CAPSULE_VERSION}. Will auto-upgrade on next command.`,
10179
- false
10180
- )
10163
+ check(Severity.INFO, `overlayMissing:${file}`, `Optional overlay ${file} missing for type "${entry.type}"`, true)
10181
10164
  );
10182
10165
  }
10183
10166
  }
10167
+ if (entry.capsuleVersion < CAPSULE_VERSION) {
10168
+ results.push(
10169
+ check(
10170
+ Severity.INFO,
10171
+ "capsuleVersionBehind",
10172
+ `Capsule version ${entry.capsuleVersion} is behind current ${CAPSULE_VERSION}. Will auto-upgrade on next command.`,
10173
+ false
10174
+ )
10175
+ );
10176
+ }
10184
10177
  return results;
10185
10178
  }
10186
10179
  var LAST_DONE_RE = /^##\s*Last done\s*\(UTC\)/im;
@@ -10191,49 +10184,41 @@ var ADHOC_RE = /\[AD-HOC\]/g;
10191
10184
  function runStatusQualityChecks(statusContent, entry) {
10192
10185
  const results = [];
10193
10186
  if (!LAST_DONE_RE.test(statusContent)) {
10194
- if (!isIgnored(entry, "lastDoneMissing")) {
10195
- results.push(
10196
- check(Severity.ERROR, "lastDoneMissing", 'STATUS.md missing "Last done (UTC)" section', true, 'Add "## Last done (UTC)" section with a timestamp to STATUS.md, or restore from .tm-backup/STATUS.md if available')
10197
- );
10198
- }
10187
+ results.push(
10188
+ check(Severity.ERROR, "lastDoneMissing", "Status file is missing the last activity section", true, "The AI will fix this on next interaction")
10189
+ );
10199
10190
  } else {
10200
10191
  const lastDoneIndex = statusContent.search(LAST_DONE_RE);
10201
10192
  const sectionAfter = statusContent.slice(lastDoneIndex);
10202
10193
  const nextSectionIndex = sectionAfter.indexOf("\n## ", 1);
10203
10194
  const lastDoneSection = nextSectionIndex > 0 ? sectionAfter.slice(0, nextSectionIndex) : sectionAfter;
10204
10195
  if (!TIMESTAMP_RE.test(lastDoneSection)) {
10205
- if (!isIgnored(entry, "lastDoneNoTimestamp")) {
10206
- results.push(
10207
- check(Severity.ERROR, "lastDoneNoTimestamp", 'STATUS.md "Last done" section has no timestamp', true, 'Add a YYYY-MM-DDTHH:MM timestamp under "Last done (UTC)" in STATUS.md, or restore from .tm-backup/STATUS.md if available')
10208
- );
10209
- }
10196
+ results.push(
10197
+ check(Severity.ERROR, "lastDoneNoTimestamp", "Last activity has no timestamp", true, "The AI will fix this on next interaction")
10198
+ );
10210
10199
  } else if (entry.status === "active") {
10211
10200
  const tsMatch = lastDoneSection.match(TIMESTAMP_RE);
10212
10201
  if (tsMatch) {
10213
10202
  const ts = new Date(tsMatch[0]);
10214
10203
  const ageDays = (Date.now() - ts.getTime()) / (1e3 * 60 * 60 * 24);
10215
10204
  if (ageDays > 3) {
10216
- if (!isIgnored(entry, "lastDoneStale")) {
10217
- results.push(
10218
- check(
10219
- Severity.WARN,
10220
- "lastDoneStale",
10221
- `STATUS.md "Last done" timestamp is ${Math.floor(ageDays)} days old`,
10222
- false,
10223
- 'Update the "Last done (UTC)" timestamp in STATUS.md or /tm snooze 7d'
10224
- )
10225
- );
10226
- }
10205
+ results.push(
10206
+ check(
10207
+ Severity.WARN,
10208
+ "lastDoneStale",
10209
+ `No activity for ${Math.floor(ageDays)} days`,
10210
+ false,
10211
+ "Send a message to resume, or /tm snooze 7d to silence"
10212
+ )
10213
+ );
10227
10214
  }
10228
10215
  }
10229
10216
  }
10230
10217
  }
10231
10218
  if (!NEXT_ACTIONS_RE.test(statusContent)) {
10232
- if (!isIgnored(entry, "nextActionsMissing")) {
10233
- results.push(
10234
- check(Severity.ERROR, "nextActionsMissing", 'STATUS.md missing "Next actions (now)" section', true, 'Add "## Next actions (now)" section with task IDs to STATUS.md')
10235
- );
10236
- }
10219
+ results.push(
10220
+ check(Severity.ERROR, "nextActionsMissing", "Status file is missing the next actions section", true, "The AI will fix this on next interaction")
10221
+ );
10237
10222
  } else {
10238
10223
  const nextActionsIndex = statusContent.search(NEXT_ACTIONS_RE);
10239
10224
  const sectionAfter = statusContent.slice(nextActionsIndex);
@@ -10242,17 +10227,15 @@ function runStatusQualityChecks(statusContent, entry) {
10242
10227
  const taskIds = nextActionsSection.match(TASK_ID_RE) ?? [];
10243
10228
  const adhocs = nextActionsSection.match(ADHOC_RE) ?? [];
10244
10229
  if (taskIds.length === 0 && adhocs.length === 0) {
10245
- if (!isIgnored(entry, "nextActionsEmpty")) {
10246
- results.push(
10247
- check(
10248
- Severity.WARN,
10249
- "nextActionsEmpty",
10250
- '"Next actions (now)" has no task IDs or entries',
10251
- false,
10252
- 'Add task IDs like [T-1] under "Next actions (now)" in STATUS.md'
10253
- )
10254
- );
10255
- }
10230
+ results.push(
10231
+ check(
10232
+ Severity.WARN,
10233
+ "nextActionsEmpty",
10234
+ "No next actions defined yet",
10235
+ false,
10236
+ "Send a message with your next task to get started"
10237
+ )
10238
+ );
10256
10239
  }
10257
10240
  }
10258
10241
  return results;
@@ -10273,9 +10256,9 @@ function runNextVsTodoChecks(statusContent, todoContent) {
10273
10256
  check(
10274
10257
  Severity.WARN,
10275
10258
  "nextNotInTodo",
10276
- `${missing.length} task IDs in "Next actions (now)" not found in TODO.md: ${missing.join(", ")}`,
10259
+ `${missing.length} tasks referenced in next actions don't exist in the TODO list: ${missing.join(", ")}`,
10277
10260
  false,
10278
- "Add missing task IDs to TODO.md or remove stale ones from STATUS.md"
10261
+ "The AI will clean these up on next interaction"
10279
10262
  )
10280
10263
  );
10281
10264
  }
@@ -10286,21 +10269,17 @@ function runCommandsLinksChecks(entry, capsuleFiles) {
10286
10269
  if (entry.type === "coding") {
10287
10270
  const commandsContent = capsuleFiles.get("COMMANDS.md");
10288
10271
  if (commandsContent !== void 0 && isEffectivelyEmpty(commandsContent)) {
10289
- if (!isIgnored(entry, "commandsEmpty")) {
10290
- results.push(
10291
- check(Severity.INFO, "commandsEmpty", "COMMANDS.md is empty for a coding topic", false, "Add build/test/deploy commands to COMMANDS.md")
10292
- );
10293
- }
10272
+ results.push(
10273
+ check(Severity.INFO, "commandsEmpty", "COMMANDS.md is empty for a coding topic", false, "Add build/test/deploy commands to COMMANDS.md")
10274
+ );
10294
10275
  }
10295
10276
  }
10296
10277
  if (entry.type === "coding" || entry.type === "research") {
10297
10278
  const linksContent = capsuleFiles.get("LINKS.md");
10298
10279
  if (linksContent !== void 0 && isEffectivelyEmpty(linksContent)) {
10299
- if (!isIgnored(entry, "linksEmpty")) {
10300
- results.push(
10301
- check(Severity.INFO, "linksEmpty", "LINKS.md is empty for a coding/research topic", false, "Add URLs and endpoints to LINKS.md")
10302
- );
10303
- }
10280
+ results.push(
10281
+ check(Severity.INFO, "linksEmpty", "LINKS.md is empty for a coding/research topic", false, "Add URLs and endpoints to LINKS.md")
10282
+ );
10304
10283
  }
10305
10284
  }
10306
10285
  return results;
@@ -10317,7 +10296,7 @@ function runCronChecks(cronContent, cronJobsPath) {
10317
10296
  const hasJobIds = lines.some((line) => JOB_ID_RE.test(line));
10318
10297
  if (!hasJobIds) {
10319
10298
  results.push(
10320
- check(Severity.WARN, "cronNoJobIds", "CRON.md lists jobs but has no recognizable job IDs", false)
10299
+ check(Severity.WARN, "cronNoJobIds", "Scheduled jobs are listed but have no recognizable job IDs", false)
10321
10300
  );
10322
10301
  return results;
10323
10302
  }
@@ -10333,7 +10312,7 @@ function runCronChecks(cronContent, cronJobsPath) {
10333
10312
  check(
10334
10313
  Severity.WARN,
10335
10314
  "cronJobNotFound",
10336
- `Job ID "${match[0]}" from CRON.md not found in cron/jobs.json`,
10315
+ `Scheduled job "${match[0]}" not found in the jobs registry`,
10337
10316
  false
10338
10317
  )
10339
10318
  );
@@ -10354,36 +10333,28 @@ function runConfigChecks(entry, includeContent, registry) {
10354
10333
  }
10355
10334
  const groupConfig = includeObj[entry.groupId];
10356
10335
  if (!groupConfig) {
10357
- if (!isIgnored(entry, "configGroupMissing")) {
10358
- results.push(
10359
- check(Severity.WARN, "configGroupMissing", `Group ${entry.groupId} missing from generated include`, false, "Run /tm sync to add group to generated include")
10360
- );
10361
- }
10336
+ results.push(
10337
+ check(Severity.WARN, "configGroupMissing", "Config is out of sync", false, "Run /tm sync to fix")
10338
+ );
10362
10339
  return results;
10363
10340
  }
10364
10341
  const topics = groupConfig["topics"];
10365
10342
  const topicConfig = topics?.[entry.threadId];
10366
10343
  if (!topicConfig) {
10367
- if (!isIgnored(entry, "configTopicMissing")) {
10368
- results.push(
10369
- check(Severity.WARN, "configTopicMissing", `Topic config missing for thread ${entry.threadId}`, false, "Run /tm sync to add topic config to generated include")
10370
- );
10371
- }
10344
+ results.push(
10345
+ check(Severity.WARN, "configTopicMissing", "Topic not found in system config", false, "Run /tm sync to fix")
10346
+ );
10372
10347
  return results;
10373
10348
  }
10374
10349
  if (!topicConfig["systemPrompt"]) {
10375
- if (!isIgnored(entry, "configNoSystemPrompt")) {
10376
- results.push(
10377
- check(Severity.WARN, "configNoSystemPrompt", "Per-topic systemPrompt is missing in generated include", false, "Run /tm sync to regenerate systemPrompt in config")
10378
- );
10379
- }
10350
+ results.push(
10351
+ check(Severity.WARN, "configNoSystemPrompt", "AI instructions are missing for this topic", false, "Run /tm sync to fix")
10352
+ );
10380
10353
  }
10381
10354
  if (!topicConfig["skills"] || !Array.isArray(topicConfig["skills"])) {
10382
- if (!isIgnored(entry, "configNoSkills")) {
10383
- results.push(
10384
- check(Severity.WARN, "configNoSkills", "Per-topic skills list is missing in generated include", false, "Run /tm sync to regenerate skills list in config")
10385
- );
10386
- }
10355
+ results.push(
10356
+ check(Severity.WARN, "configNoSkills", "Command list is missing for this topic", false, "Run /tm sync to fix")
10357
+ );
10387
10358
  }
10388
10359
  return results;
10389
10360
  }
@@ -10395,9 +10366,9 @@ function runIncludeDriftCheck(includeFileContent, registry) {
10395
10366
  check(
10396
10367
  Severity.WARN,
10397
10368
  "includeDrift",
10398
- "Generated include file has no registry-hash comment. Run /tm sync.",
10369
+ "Config is out of sync with your topics",
10399
10370
  false,
10400
- "Run /tm sync to regenerate the config include"
10371
+ "Run /tm sync to fix"
10401
10372
  )
10402
10373
  );
10403
10374
  return results;
@@ -10408,9 +10379,9 @@ function runIncludeDriftCheck(includeFileContent, registry) {
10408
10379
  check(
10409
10380
  Severity.WARN,
10410
10381
  "includeDrift",
10411
- "Generated include is out of sync with registry. Run /tm sync.",
10382
+ "Config is out of sync with your topics",
10412
10383
  false,
10413
- "Run /tm sync to regenerate the config include"
10384
+ "Run /tm sync to fix"
10414
10385
  )
10415
10386
  );
10416
10387
  }
@@ -10423,9 +10394,9 @@ function runSpamControlCheck(entry) {
10423
10394
  check(
10424
10395
  Severity.INFO,
10425
10396
  "spamControl",
10426
- `${entry.consecutiveSilentDoctors} consecutive doctor reports with no user interaction. Auto-snoozing for 30 days.`,
10397
+ `No activity for a while \u2014 auto-snoozing for 30 days`,
10427
10398
  true,
10428
- "Interact with the topic or /tm snooze 30d to silence reports"
10399
+ "Send a message to resume"
10429
10400
  )
10430
10401
  );
10431
10402
  }
@@ -10442,7 +10413,7 @@ function runAllChecksForTopic(entry, projectsBase, includeContent, registry, cro
10442
10413
  if (statusContent) {
10443
10414
  results.push(...runStatusQualityChecks(statusContent, entry));
10444
10415
  const todoContent = capsuleFiles.get("TODO.md");
10445
- if (todoContent && !isIgnored(entry, "nextNotInTodo")) {
10416
+ if (todoContent) {
10446
10417
  results.push(...runNextVsTodoChecks(statusContent, todoContent));
10447
10418
  }
10448
10419
  }
@@ -10526,11 +10497,11 @@ async function handleDoctor(ctx) {
10526
10497
  }
10527
10498
  const projectsBase = path8.join(workspaceDir, "projects");
10528
10499
  if (!jailCheck(projectsBase, entry.slug)) {
10529
- return { text: "Path safety check failed." };
10500
+ return { text: "Something went wrong \u2014 path validation failed." };
10530
10501
  }
10531
10502
  const capsuleDir = path8.join(projectsBase, entry.slug);
10532
10503
  if (rejectSymlink(capsuleDir)) {
10533
- return { text: "Capsule directory is a symlink. Aborting for security." };
10504
+ return { text: "Something went wrong \u2014 detected an unsafe file system configuration." };
10534
10505
  }
10535
10506
  let includeContent;
10536
10507
  const incPath = includePath(configDir);
@@ -10556,13 +10527,6 @@ async function handleDoctor(ctx) {
10556
10527
  registry.callbackSecret,
10557
10528
  userId
10558
10529
  );
10559
- const textCommands = [
10560
- "",
10561
- "Or use text commands:",
10562
- "/tm snooze 7d",
10563
- "/tm snooze 30d",
10564
- "/tm archive"
10565
- ].join("\n");
10566
10530
  await withRegistry(workspaceDir, (data) => {
10567
10531
  const topic = data.topics[key];
10568
10532
  if (topic) {
@@ -10570,7 +10534,7 @@ async function handleDoctor(ctx) {
10570
10534
  }
10571
10535
  });
10572
10536
  return {
10573
- text: reportText + textCommands,
10537
+ text: reportText,
10574
10538
  inlineKeyboard: keyboard
10575
10539
  };
10576
10540
  }
@@ -10594,7 +10558,7 @@ async function handleDoctorAll(ctx) {
10594
10558
  if (elapsed < DOCTOR_ALL_COOLDOWN_MS) {
10595
10559
  const remainingMin = Math.ceil((DOCTOR_ALL_COOLDOWN_MS - elapsed) / 6e4);
10596
10560
  return {
10597
- text: `Doctor-all was run ${Math.floor(elapsed / 6e4)} minutes ago. Try again in ${remainingMin} minute(s).`
10561
+ text: `Health checks were run ${Math.floor(elapsed / 6e4)} minutes ago. Try again in ${remainingMin} minute(s).`
10598
10562
  };
10599
10563
  }
10600
10564
  }
@@ -10727,11 +10691,11 @@ async function handleDoctorAll(ctx) {
10727
10691
  }
10728
10692
  });
10729
10693
  const lines = [
10730
- `**Doctor All Summary**`,
10694
+ `**Health Check Summary**`,
10731
10695
  "",
10732
- `Processed: ${processed}`,
10733
- `Skipped (ineligible): ${skipped}`,
10734
- `Total: ${allEntries.length}`
10696
+ `Checked: ${processed}`,
10697
+ `Skipped: ${skipped}`,
10698
+ `Total topics: ${allEntries.length}`
10735
10699
  ];
10736
10700
  if (ctx.postFn) {
10737
10701
  lines.push(`Posted: ${postSuccesses}, Post failures: ${postErrors}`);
@@ -10748,10 +10712,7 @@ async function handleDoctorAll(ctx) {
10748
10712
  }
10749
10713
  if (migrationGroups.length > 0) {
10750
10714
  lines.push("");
10751
- lines.push("**Possible group migrations detected:**");
10752
- for (const gid of migrationGroups) {
10753
- lines.push(`- Group ${gid}: all topics failed. Check for group migration.`);
10754
- }
10715
+ lines.push(`**Warning:** ${migrationGroups.length} group(s) had all topics fail. The group may have been migrated or deleted.`);
10755
10716
  }
10756
10717
  return {
10757
10718
  text: lines.join("\n")
@@ -10793,6 +10754,61 @@ async function handleList(ctx) {
10793
10754
  // src/commands/status.ts
10794
10755
  import * as fs10 from "node:fs";
10795
10756
  import * as path10 from "node:path";
10757
+ var LAST_DONE_RE2 = /^##\s*Last done\s*\(UTC\)\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im;
10758
+ var NEXT_ACTIONS_RE2 = /^##\s*Next (?:3 )?actions(?: \(now\))?\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im;
10759
+ var UPCOMING_RE = /^##\s*Upcoming actions\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im;
10760
+ var ISO_RE = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
10761
+ function extractTimestamp(content) {
10762
+ const match = content.match(LAST_DONE_RE2);
10763
+ if (!match) return null;
10764
+ const iso = match[1]?.match(ISO_RE);
10765
+ return iso ? iso[0] : null;
10766
+ }
10767
+ function extractSection(content, re) {
10768
+ const match = content.match(re);
10769
+ if (!match) return "";
10770
+ return match[1]?.trim() ?? "";
10771
+ }
10772
+ function isPlaceholder(text) {
10773
+ if (!text) return true;
10774
+ const stripped = text.replace(/[_*]/g, "").trim().toLowerCase();
10775
+ return stripped === "none yet." || stripped === "none yet" || stripped === "" || stripped.startsWith("e.g.");
10776
+ }
10777
+ function formatSection(raw) {
10778
+ if (isPlaceholder(raw)) return "_None yet._";
10779
+ return raw;
10780
+ }
10781
+ function formatStatus(name, content) {
10782
+ const timestamp = extractTimestamp(content);
10783
+ const nextRaw = extractSection(content, NEXT_ACTIONS_RE2);
10784
+ const upcomingRaw = extractSection(content, UPCOMING_RE);
10785
+ const doneMatch = content.match(LAST_DONE_RE2);
10786
+ let lastDoneBody = "";
10787
+ if (doneMatch) {
10788
+ const section = doneMatch[1]?.trim() ?? "";
10789
+ lastDoneBody = section.replace(ISO_RE, "").trim();
10790
+ }
10791
+ const lines = [
10792
+ `**${name}**`,
10793
+ ""
10794
+ ];
10795
+ if (timestamp) {
10796
+ lines.push(`**Last activity:** ${relativeTime(timestamp)}`);
10797
+ }
10798
+ if (lastDoneBody && !isPlaceholder(lastDoneBody)) {
10799
+ lines.push(lastDoneBody);
10800
+ }
10801
+ lines.push("");
10802
+ lines.push("**Next actions**");
10803
+ lines.push(formatSection(nextRaw));
10804
+ const upcomingFormatted = formatSection(upcomingRaw);
10805
+ if (upcomingFormatted !== "_None yet._") {
10806
+ lines.push("");
10807
+ lines.push("**Upcoming**");
10808
+ lines.push(upcomingFormatted);
10809
+ }
10810
+ return lines.join("\n");
10811
+ }
10796
10812
  async function handleStatus(ctx) {
10797
10813
  const { workspaceDir, userId, groupId, threadId } = ctx;
10798
10814
  if (!userId || !groupId || !threadId) {
@@ -10811,23 +10827,23 @@ async function handleStatus(ctx) {
10811
10827
  const projectsBase = path10.join(workspaceDir, "projects");
10812
10828
  const capsuleDir = path10.join(projectsBase, entry.slug);
10813
10829
  if (!jailCheck(projectsBase, entry.slug)) {
10814
- return { text: "Path safety check failed." };
10830
+ return { text: "Something went wrong \u2014 path validation failed." };
10815
10831
  }
10816
10832
  if (rejectSymlink(capsuleDir)) {
10817
- return { text: "Capsule directory is a symlink. Aborting for security." };
10833
+ return { text: "Something went wrong \u2014 detected an unsafe file system configuration." };
10818
10834
  }
10819
10835
  const statusPath = path10.join(capsuleDir, "STATUS.md");
10820
10836
  if (!fs10.existsSync(statusPath)) {
10821
- return { text: "STATUS.md not found in capsule. Run /tm doctor to diagnose." };
10837
+ return { text: "No status available yet. Run /tm doctor to diagnose." };
10822
10838
  }
10823
10839
  try {
10824
10840
  const content = fs10.readFileSync(statusPath, "utf-8");
10825
10841
  return {
10826
- text: truncateMessage(content)
10842
+ text: truncateMessage(formatStatus(entry.name, content))
10827
10843
  };
10828
10844
  } catch (err) {
10829
10845
  const msg = err instanceof Error ? err.message : String(err);
10830
- return { text: `Failed to read STATUS.md: ${msg}` };
10846
+ return { text: `Failed to read topic status: ${msg}` };
10831
10847
  }
10832
10848
  }
10833
10849
 
@@ -10852,7 +10868,7 @@ async function handleSync(ctx) {
10852
10868
  }
10853
10869
  const restartResult = await triggerRestart(rpc, logger);
10854
10870
  const topicCount = Object.keys(registry.topics).length;
10855
- let text = `Include regenerated from ${topicCount} topic(s). Config synced.`;
10871
+ let text = `Config synced for ${topicCount} topic(s).`;
10856
10872
  if (!restartResult.success && restartResult.fallbackMessage) {
10857
10873
  text += "\n" + restartResult.fallbackMessage;
10858
10874
  }
@@ -10869,7 +10885,7 @@ async function handleRename(ctx, newName) {
10869
10885
  }
10870
10886
  const trimmedName = newName.trim();
10871
10887
  if (!trimmedName) {
10872
- return { text: "Usage: /tm rename <new-name>" };
10888
+ return { text: "What should the new name be? Example: /tm rename my-project" };
10873
10889
  }
10874
10890
  if (trimmedName.length > MAX_NAME_LENGTH) {
10875
10891
  return { text: `Name too long (max ${MAX_NAME_LENGTH} characters).` };
@@ -10914,11 +10930,8 @@ Warning: include generation failed: ${msg}`;
10914
10930
  workspaceDir,
10915
10931
  buildAuditEntry(userId, "rename", entry.slug, `Renamed from "${oldName}" to "${trimmedName}"`)
10916
10932
  );
10917
- const topicCard = buildTopicCard(trimmedName, entry.slug, entry.type, entry.capsuleVersion);
10918
10933
  return {
10919
- text: `Topic renamed from **${oldName}** to **${trimmedName}**.
10920
-
10921
- ${topicCard}${restartMsg}`
10934
+ text: `Topic renamed from **${oldName}** to **${trimmedName}**.${restartMsg}`
10922
10935
  };
10923
10936
  }
10924
10937
 
@@ -10941,7 +10954,7 @@ async function handleUpgrade(ctx) {
10941
10954
  }
10942
10955
  if (entry.capsuleVersion >= CAPSULE_VERSION) {
10943
10956
  return {
10944
- text: `Topic **${entry.name}** is already at capsule version ${CAPSULE_VERSION}. No upgrade needed.`
10957
+ text: `Topic **${entry.name}** is already up to date. No upgrade needed.`
10945
10958
  };
10946
10959
  }
10947
10960
  const projectsBase = path11.join(workspaceDir, "projects");
@@ -10960,7 +10973,7 @@ async function handleUpgrade(ctx) {
10960
10973
  const addedList = result.addedFiles.length > 0 ? `
10961
10974
  Added files: ${result.addedFiles.join(", ")}` : "\nNo new files added.";
10962
10975
  return {
10963
- text: `Topic **${entry.name}** upgraded from v${entry.capsuleVersion} to v${result.newVersion}.${addedList}`
10976
+ text: `Topic **${entry.name}** upgraded.${addedList}`
10964
10977
  };
10965
10978
  }
10966
10979
 
@@ -10973,7 +10986,7 @@ async function handleSnooze(ctx, args) {
10973
10986
  }
10974
10987
  const trimmed = args.trim();
10975
10988
  if (!trimmed) {
10976
- return { text: "Usage: /tm snooze <Nd> (e.g., 7d, 30d)" };
10989
+ return { text: "How long to snooze? Example: /tm snooze 7d" };
10977
10990
  }
10978
10991
  const match = DURATION_RE.exec(trimmed);
10979
10992
  if (!match) {
@@ -11007,7 +11020,7 @@ async function handleSnooze(ctx, args) {
11007
11020
  buildAuditEntry(userId, "snooze", entry.slug, `Snoozed for ${days} days until ${snoozeUntil}`)
11008
11021
  );
11009
11022
  return {
11010
- text: `Topic **${entry.name}** snoozed for ${days} days (until ${snoozeUntil}).`
11023
+ text: `Topic **${entry.name}** snoozed for ${days} days. Health checks will resume automatically after that.`
11011
11024
  };
11012
11025
  }
11013
11026
 
@@ -11137,7 +11150,7 @@ async function handleEnable(ctx) {
11137
11150
  data.autopilotEnabled = true;
11138
11151
  });
11139
11152
  return {
11140
- text: "**Autopilot enabled.**\nDaily health sweeps will run via the OpenClaw heartbeat."
11153
+ text: "**Autopilot enabled.**\nHealth checks will run automatically every day."
11141
11154
  };
11142
11155
  }
11143
11156
  async function handleDisable(ctx) {
@@ -11147,14 +11160,14 @@ async function handleDisable(ctx) {
11147
11160
  await withRegistry(workspaceDir, (data) => {
11148
11161
  data.autopilotEnabled = false;
11149
11162
  });
11150
- return { text: "Autopilot is not enabled (no HEARTBEAT.md found)." };
11163
+ return { text: "Autopilot is already disabled." };
11151
11164
  }
11152
11165
  let content = fs11.readFileSync(heartbeatPath, "utf-8");
11153
11166
  if (!content.includes(MARKER_START)) {
11154
11167
  await withRegistry(workspaceDir, (data) => {
11155
11168
  data.autopilotEnabled = false;
11156
11169
  });
11157
- return { text: "Autopilot is not enabled (no marker found in HEARTBEAT.md)." };
11170
+ return { text: "Autopilot is already disabled." };
11158
11171
  }
11159
11172
  const startIdx = content.indexOf(MARKER_START);
11160
11173
  const endIdx = content.indexOf(MARKER_END);
@@ -11172,17 +11185,17 @@ async function handleDisable(ctx) {
11172
11185
  data.autopilotEnabled = false;
11173
11186
  });
11174
11187
  return {
11175
- text: "**Autopilot disabled.**\nDaily sweeps will no longer run automatically."
11188
+ text: "**Autopilot disabled.**\nAutomatic health checks are now off."
11176
11189
  };
11177
11190
  }
11178
11191
  async function handleStatus2(ctx) {
11179
11192
  const { workspaceDir } = ctx;
11180
11193
  const registry = readRegistry(workspaceDir);
11181
11194
  const enabled = registry.autopilotEnabled;
11182
- const lastRun = registry.lastDoctorAllRunAt ?? "never";
11195
+ const lastRun = registry.lastDoctorAllRunAt ? relativeTime(registry.lastDoctorAllRunAt) : "never";
11183
11196
  const lines = [
11184
11197
  `**Autopilot:** ${enabled ? "enabled" : "disabled"}`,
11185
- `**Last doctor-all run:** ${lastRun}`
11198
+ `**Last health check run:** ${lastRun}`
11186
11199
  ];
11187
11200
  return {
11188
11201
  text: lines.join("\n")
@@ -11203,8 +11216,8 @@ async function handleDailyReport(ctx) {
11203
11216
  if (!entry) {
11204
11217
  return { text: "This topic is not registered. Run /tm init first." };
11205
11218
  }
11206
- if (entry.lastDoctorReportAt) {
11207
- const lastReport = new Date(entry.lastDoctorReportAt);
11219
+ if (entry.lastDailyReportAt) {
11220
+ const lastReport = new Date(entry.lastDailyReportAt);
11208
11221
  const now = /* @__PURE__ */ new Date();
11209
11222
  if (lastReport.getUTCFullYear() === now.getUTCFullYear() && lastReport.getUTCMonth() === now.getUTCMonth() && lastReport.getUTCDate() === now.getUTCDate()) {
11210
11223
  return { text: "Daily report already generated today. Try again tomorrow." };
@@ -11213,7 +11226,7 @@ async function handleDailyReport(ctx) {
11213
11226
  const projectsBase = path13.join(workspaceDir, "projects");
11214
11227
  const capsuleDir = path13.join(projectsBase, entry.slug);
11215
11228
  if (!fs12.existsSync(capsuleDir)) {
11216
- return { text: `Capsule directory not found: projects/${entry.slug}/` };
11229
+ return { text: "Topic files not found. Run /tm init to set up this topic." };
11217
11230
  }
11218
11231
  const statusContent = readFileOrNull(path13.join(capsuleDir, "STATUS.md"));
11219
11232
  const todoContent = readFileOrNull(path13.join(capsuleDir, "TODO.md"));
@@ -11224,7 +11237,7 @@ async function handleDailyReport(ctx) {
11224
11237
  const nextContent = extractNextActions(statusContent);
11225
11238
  const upcomingContent = extractUpcoming(statusContent);
11226
11239
  const health = computeHealth(entry.lastMessageAt, statusContent, blockers);
11227
- const reportText = buildDailyReport({
11240
+ const reportData = {
11228
11241
  name: entry.name,
11229
11242
  doneContent,
11230
11243
  learningsContent: newLearnings,
@@ -11232,14 +11245,15 @@ async function handleDailyReport(ctx) {
11232
11245
  nextContent,
11233
11246
  upcomingContent,
11234
11247
  health
11235
- });
11248
+ };
11236
11249
  if (ctx.postFn) {
11237
11250
  try {
11238
- await ctx.postFn(groupId, threadId, reportText);
11251
+ const htmlReport = buildDailyReport(reportData, "html");
11252
+ await ctx.postFn(groupId, threadId, htmlReport);
11239
11253
  await withRegistry(workspaceDir, (data) => {
11240
11254
  const e = data.topics[key];
11241
11255
  if (e) {
11242
- e.lastDoctorReportAt = (/* @__PURE__ */ new Date()).toISOString();
11256
+ e.lastDailyReportAt = (/* @__PURE__ */ new Date()).toISOString();
11243
11257
  }
11244
11258
  });
11245
11259
  } catch (err) {
@@ -11251,11 +11265,11 @@ async function handleDailyReport(ctx) {
11251
11265
  await withRegistry(workspaceDir, (data) => {
11252
11266
  const e = data.topics[key];
11253
11267
  if (e) {
11254
- e.lastDoctorReportAt = (/* @__PURE__ */ new Date()).toISOString();
11268
+ e.lastDailyReportAt = (/* @__PURE__ */ new Date()).toISOString();
11255
11269
  }
11256
11270
  });
11257
11271
  }
11258
- return { text: reportText };
11272
+ return { text: buildDailyReport(reportData, "markdown") };
11259
11273
  }
11260
11274
  function readFileOrNull(filePath) {
11261
11275
  try {
@@ -11505,33 +11519,16 @@ async function handleCallback(data, ctx) {
11505
11519
  return { text: "Topic not found." };
11506
11520
  }
11507
11521
  switch (action) {
11508
- case "fix":
11509
- return handleCallbackFix(cbCtx);
11510
11522
  case "snooze7d":
11511
11523
  return handleSnooze(cbCtx, "7d");
11512
11524
  case "snooze30d":
11513
11525
  return handleSnooze(cbCtx, "30d");
11514
11526
  case "archive":
11515
11527
  return handleArchive(cbCtx);
11516
- case "ignore": {
11517
- return {
11518
- text: `To ignore a specific check, use: /tm snooze or contact an admin. The "Ignore" action requires specifying a check ID.`
11519
- };
11520
- }
11521
11528
  default:
11522
11529
  return { text: `Unknown callback action: ${action}` };
11523
11530
  }
11524
11531
  }
11525
- async function handleCallbackFix(ctx) {
11526
- const { userId, workspaceDir } = ctx;
11527
- if (userId) {
11528
- appendAudit(
11529
- workspaceDir,
11530
- buildAuditEntry(userId, "doctor fix", "callback", "Fix callback triggered")
11531
- );
11532
- }
11533
- return handleDoctor(ctx);
11534
- }
11535
11532
 
11536
11533
  // src/index.ts
11537
11534
  function resolveConfigDir() {