clawvault 2.3.1 → 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 (40) hide show
  1. package/bin/register-core-commands.js +25 -2
  2. package/bin/register-task-commands.js +18 -2
  3. package/bin/register-task-commands.test.js +24 -0
  4. package/dist/{chunk-TB3BM2PQ.js → chunk-33GW63WK.js} +4 -35
  5. package/dist/{chunk-JVAWKNIZ.js → chunk-AHGUJG76.js} +1 -1
  6. package/dist/{chunk-TT3FXYCN.js → chunk-BI6SGGZP.js} +1 -1
  7. package/dist/{chunk-NGVAEFT2.js → chunk-DEFBIVQ3.js} +21 -0
  8. package/dist/{chunk-USZU5CBB.js → chunk-DHJPXGC7.js} +1 -1
  9. package/dist/{chunk-2AYPFUGX.js → chunk-FEFPBHH4.js} +286 -11
  10. package/dist/{chunk-K6XHCUFL.js → chunk-FHFUXL6G.js} +8 -1
  11. package/dist/{chunk-RARDNTUP.js → chunk-GBIDDDSL.js} +2 -2
  12. package/dist/chunk-IFTEGE4D.js +361 -0
  13. package/dist/{chunk-VBVEXNI5.js → chunk-JXY6T5R7.js} +1 -1
  14. package/dist/chunk-L3DJ36BZ.js +40 -0
  15. package/dist/{chunk-OTQW3OMC.js → chunk-Q3WBH4P4.js} +97 -0
  16. package/dist/{chunk-6AQZIPLV.js → chunk-SNEMCQP7.js} +12 -5
  17. package/dist/commands/backlog.js +9 -2
  18. package/dist/commands/blocked.js +9 -2
  19. package/dist/commands/canvas.d.ts +11 -3
  20. package/dist/commands/canvas.js +954 -20
  21. package/dist/commands/context.js +4 -3
  22. package/dist/commands/doctor.js +1 -1
  23. package/dist/commands/migrate-observations.js +2 -2
  24. package/dist/commands/observe.d.ts +1 -0
  25. package/dist/commands/observe.js +4 -3
  26. package/dist/commands/rebuild.js +4 -3
  27. package/dist/commands/reflect.js +3 -3
  28. package/dist/commands/replay.js +5 -4
  29. package/dist/commands/setup.d.ts +10 -2
  30. package/dist/commands/setup.js +1 -1
  31. package/dist/commands/sleep.js +5 -4
  32. package/dist/commands/status.js +1 -1
  33. package/dist/commands/task.js +1 -1
  34. package/dist/commands/wake.js +3 -3
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.js +13 -11
  37. package/dist/lib/task-utils.d.ts +13 -1
  38. package/dist/lib/task-utils.js +3 -1
  39. package/package.json +1 -1
  40. package/dist/chunk-W463YRED.js +0 -97
@@ -13,6 +13,13 @@ export function registerCoreCommands(
13
13
  .option('-n, --name <name>', 'Vault name')
14
14
  .option('--qmd', 'Set up qmd semantic search collection')
15
15
  .option('--qmd-collection <name>', 'qmd collection name (defaults to vault name)')
16
+ .option('--no-bases', 'Skip Obsidian Bases file generation')
17
+ .option('--no-tasks', 'Skip tasks/ and backlog/ directories')
18
+ .option('--no-graph', 'Skip initial graph build')
19
+ .option('--categories <list>', 'Comma-separated list of custom categories to create')
20
+ .option('--canvas <template>', 'Generate a canvas dashboard on init (default, brain, project-board, sprint)')
21
+ .option('--theme <style>', 'Graph color theme to apply (neural, minimal, none)', 'none')
22
+ .option('--minimal', 'Create minimal vault (memory categories only, no tasks/bases/graph)')
16
23
  .action(async (vaultPath, options) => {
17
24
  const targetPath = vaultPath || '.';
18
25
  console.log(chalk.cyan(`\n🐘 Initializing ClawVault at ${path.resolve(targetPath)}...\n`));
@@ -67,10 +74,26 @@ export function registerCoreCommands(
67
74
  program
68
75
  .command('setup')
69
76
  .description('Auto-discover and configure a ClawVault')
70
- .action(async () => {
77
+ .option('--graph-colors', 'Set up graph color scheme for Obsidian')
78
+ .option('--no-graph-colors', 'Skip graph color configuration')
79
+ .option('--bases', 'Generate Obsidian Bases views for task management')
80
+ .option('--no-bases', 'Skip Bases file generation')
81
+ .option('--canvas [template]', 'Generate canvas dashboard (default, brain, project-board, sprint)')
82
+ .option('--no-canvas', 'Skip canvas generation')
83
+ .option('--theme <style>', 'Graph color theme (neural, minimal, none)', 'neural')
84
+ .option('--force', 'Overwrite existing configuration files')
85
+ .option('-v, --vault <path>', 'Vault path')
86
+ .action(async (options) => {
71
87
  try {
72
88
  const { setupCommand } = await import('../dist/commands/setup.js');
73
- await setupCommand();
89
+ await setupCommand({
90
+ graphColors: options.graphColors,
91
+ bases: options.bases,
92
+ canvas: options.canvas,
93
+ theme: options.theme,
94
+ force: options.force,
95
+ vault: options.vault
96
+ });
74
97
  } catch (err) {
75
98
  console.error(chalk.red(`Error: ${err.message}`));
76
99
  process.exit(1);
@@ -242,12 +242,28 @@ export function registerTaskCommands(
242
242
  .description('Generate Obsidian canvas dashboard')
243
243
  .option('-v, --vault <path>', 'Vault path')
244
244
  .option('--output <path>', 'Output file path (default: dashboard.canvas)')
245
+ .option('--template <id>', 'Canvas template ID (default, project-board, brain, sprint)')
246
+ .option('--project <project>', 'Project filter for template-aware canvases')
247
+ .option('--owner <owner>', 'Filter tasks by owner (agent name or human)')
248
+ .option('--width <pixels>', 'Canvas width in pixels', parseInt)
249
+ .option('--height <pixels>', 'Canvas height in pixels', parseInt)
250
+ .option('--include-done', 'Include completed tasks (default: limited)')
251
+ .option('--list-templates', 'List available canvas templates and exit')
245
252
  .action(async (options) => {
246
253
  try {
247
- const vaultPath = resolveVaultPath(options.vault);
254
+ const vaultPath = options.listTemplates
255
+ ? (options.vault || '.')
256
+ : resolveVaultPath(options.vault);
248
257
  const { canvasCommand } = await import('../dist/commands/canvas.js');
249
258
  await canvasCommand(vaultPath, {
250
- output: options.output
259
+ output: options.output,
260
+ template: options.template,
261
+ project: options.project,
262
+ owner: options.owner,
263
+ width: options.width,
264
+ height: options.height,
265
+ includeDone: options.includeDone,
266
+ listTemplates: options.listTemplates
251
267
  });
252
268
  } catch (err) {
253
269
  console.error(chalk.red(`Error: ${err.message}`));
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Command } from 'commander';
3
+ import { registerTaskCommands } from './register-task-commands.js';
4
+ import { chalkStub, stubResolveVaultPath } from './test-helpers/cli-command-fixtures.js';
5
+
6
+ describe('register-task-commands', () => {
7
+ it('adds canvas template and listing flags', () => {
8
+ const program = new Command();
9
+ registerTaskCommands(program, {
10
+ chalk: chalkStub,
11
+ resolveVaultPath: stubResolveVaultPath
12
+ });
13
+
14
+ const canvasCommand = program.commands.find((command) => command.name() === 'canvas');
15
+ expect(canvasCommand).toBeDefined();
16
+
17
+ const optionFlags = canvasCommand?.options.map((option) => option.flags) ?? [];
18
+ expect(optionFlags).toEqual(expect.arrayContaining([
19
+ '--template <id>',
20
+ '--list-templates',
21
+ '--project <project>'
22
+ ]));
23
+ });
24
+ });
@@ -1,48 +1,17 @@
1
1
  import {
2
2
  ClawVault
3
- } from "./chunk-OTQW3OMC.js";
3
+ } from "./chunk-Q3WBH4P4.js";
4
4
  import {
5
- parseObservationMarkdown
6
- } from "./chunk-K6XHCUFL.js";
5
+ parseObservationLines,
6
+ readObservations
7
+ } from "./chunk-L3DJ36BZ.js";
7
8
  import {
8
9
  getMemoryGraph
9
10
  } from "./chunk-ZZA73MFY.js";
10
- import {
11
- listObservationFiles
12
- } from "./chunk-NAMFB7ZA.js";
13
11
 
14
12
  // src/commands/context.ts
15
13
  import * as path from "path";
16
14
 
17
- // src/lib/observation-reader.ts
18
- import * as fs from "fs";
19
- function readObservations(vaultPath, days = 7) {
20
- const normalizedDays = Number.isFinite(days) ? Math.max(0, Math.floor(days)) : 0;
21
- if (normalizedDays === 0) {
22
- return "";
23
- }
24
- const files = listObservationFiles(vaultPath, {
25
- includeLegacy: true,
26
- includeArchive: false,
27
- dedupeByDate: true
28
- }).sort((left, right) => right.date.localeCompare(left.date)).slice(0, normalizedDays);
29
- if (files.length === 0) {
30
- return "";
31
- }
32
- return files.map((entry) => fs.readFileSync(entry.path, "utf-8").trim()).filter(Boolean).join("\n\n").trim();
33
- }
34
- function parseObservationLines(markdown) {
35
- return parseObservationMarkdown(markdown).map((record) => ({
36
- type: record.type,
37
- confidence: record.confidence,
38
- importance: record.importance,
39
- content: record.content,
40
- date: record.date,
41
- format: record.format,
42
- priority: record.priority
43
- }));
44
- }
45
-
46
15
  // src/lib/token-counter.ts
47
16
  function estimateTokens(text) {
48
17
  if (!text) {
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  normalizeObservationContent,
6
6
  parseObservationMarkdown
7
- } from "./chunk-K6XHCUFL.js";
7
+ } from "./chunk-FHFUXL6G.js";
8
8
  import {
9
9
  formatIsoWeekKey,
10
10
  getIsoWeek,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runReflection
3
- } from "./chunk-JVAWKNIZ.js";
3
+ } from "./chunk-AHGUJG76.js";
4
4
  import {
5
5
  resolveVaultPath
6
6
  } from "./chunk-MXSSG3QU.js";
@@ -147,6 +147,7 @@ function createTask(vaultPath, title, options = {}) {
147
147
  created: now,
148
148
  updated: now
149
149
  };
150
+ if (options.source) frontmatter.source = options.source;
150
151
  if (options.owner) frontmatter.owner = options.owner;
151
152
  if (options.project) frontmatter.project = options.project;
152
153
  if (options.priority) frontmatter.priority = options.priority;
@@ -264,6 +265,25 @@ ${options.content}
264
265
  path: backlogPath
265
266
  };
266
267
  }
268
+ function updateBacklogItem(vaultPath, slug, updates) {
269
+ const backlogItem = readBacklogItem(vaultPath, slug);
270
+ if (!backlogItem) {
271
+ throw new Error(`Backlog item not found: ${slug}`);
272
+ }
273
+ const newFrontmatter = {
274
+ ...backlogItem.frontmatter
275
+ };
276
+ if (updates.source !== void 0) newFrontmatter.source = updates.source;
277
+ if (updates.project !== void 0) newFrontmatter.project = updates.project;
278
+ if (updates.tags !== void 0) newFrontmatter.tags = updates.tags;
279
+ if (updates.lastSeen !== void 0) newFrontmatter.lastSeen = updates.lastSeen;
280
+ const fileContent = matter.stringify(backlogItem.content, newFrontmatter);
281
+ fs.writeFileSync(backlogItem.path, fileContent);
282
+ return {
283
+ ...backlogItem,
284
+ frontmatter: newFrontmatter
285
+ };
286
+ }
267
287
  function promoteBacklogItem(vaultPath, slug, options = {}) {
268
288
  const backlogItem = readBacklogItem(vaultPath, slug);
269
289
  if (!backlogItem) {
@@ -343,6 +363,7 @@ export {
343
363
  updateTask,
344
364
  completeTask,
345
365
  createBacklogItem,
366
+ updateBacklogItem,
346
367
  promoteBacklogItem,
347
368
  getBlockedTasks,
348
369
  getActiveTasks,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Observer
3
- } from "./chunk-2AYPFUGX.js";
3
+ } from "./chunk-FEFPBHH4.js";
4
4
  import {
5
5
  resolveVaultPath
6
6
  } from "./chunk-MXSSG3QU.js";
@@ -1,3 +1,10 @@
1
+ import {
2
+ createBacklogItem,
3
+ listBacklogItems,
4
+ listTasks,
5
+ updateBacklogItem,
6
+ updateTask
7
+ } from "./chunk-DEFBIVQ3.js";
1
8
  import {
2
9
  DATE_HEADING_RE,
3
10
  inferObservationType,
@@ -6,7 +13,7 @@ import {
6
13
  parseObservationMarkdown,
7
14
  renderObservationMarkdown,
8
15
  renderScoredObservationLine
9
- } from "./chunk-K6XHCUFL.js";
16
+ } from "./chunk-FHFUXL6G.js";
10
17
  import {
11
18
  ensureLedgerStructure,
12
19
  ensureParentDir,
@@ -20,6 +27,10 @@ import {
20
27
  var CRITICAL_RE = /(?:\b(?:decision|decided|chose|chosen|selected|picked|opted|switched to)\s*:?|\bdecid(?:e|ed|ing|ion)\b|\berror\b|\bfail(?:ed|ure|ing)?\b|\bblock(?:ed|er)?\b|\bbreaking(?:\s+change)?s?\b|\bcritical\b|\b\w+\s+chosen\s+(?:for|over|as)\b|\bpublish(?:ed)?\b.*@?\d+\.\d+|\bmerge[d]?\s+(?:PR|pull\s+request)\b|\bshipped\b|\breleased?\b.*v?\d+\.\d+|\bsigned\b.*\b(?:contract|agreement|deal)\b|\bpricing\b.*\$|\bdemo\b.*\b(?:completed?|done|finished)\b|\bmeeting\b.*\b(?:completed?|done|finished)\b|\bstrategy\b.*\b(?:pivot|change|shift)\b)/i;
21
28
  var DEADLINE_WITH_DATE_RE = /(?:(?:\bdeadline\b|\bdue(?:\s+date)?\b|\bcutoff\b).*(?:\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}(?:\/\d{2,4})?|(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\s+\d{1,2})|(?:\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}(?:\/\d{2,4})?|(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\s+\d{1,2}).*(?:\bdeadline\b|\bdue(?:\s+date)?\b|\bcutoff\b))/i;
22
29
  var NOTABLE_RE = /\b(prefer(?:ence|s)?|likes?|dislikes?|context|pattern|architecture|approach|trade[- ]?off|milestone|stakeholder|teammate|collaborat(?:e|ed|ion)|discussion|notable|deadline|due|timeline|deploy(?:ed|ment)?|built|configured|launched|proposal|pitch|onboard(?:ed|ing)?|migrat(?:e|ed|ion)|domain|DNS|infra(?:structure)?)\b/i;
30
+ var TODO_SIGNAL_RE = /(?:\btodo:\s*|\bwe need to\b|\bdon't forget(?: to)?\b|\bremember to\b|\bmake sure to\b)/i;
31
+ var COMMITMENT_TASK_SIGNAL_RE = /\b(?:i'?ll|i will|let me|(?:i'?m\s+)?going to|plan to|should)\b/i;
32
+ var UNRESOLVED_COMMITMENT_RE = /\b(?:need to figure out|tbd|to be determined)\b/i;
33
+ var DEADLINE_SIGNAL_RE = /\b(?:by\s+(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday|tomorrow)|before\s+the\s+\w+|deadline is)\b/i;
23
34
  var Compressor = class {
24
35
  model;
25
36
  now;
@@ -70,13 +81,19 @@ var Compressor = class {
70
81
  "- Output markdown only.",
71
82
  "- Group observations by date heading: ## YYYY-MM-DD",
72
83
  "- Each observation line MUST follow: - [type|c=<0.00-1.00>|i=<0.00-1.00>] <observation>",
73
- "- Allowed type tags: decision, preference, fact, commitment, milestone, lesson, relationship, project",
84
+ "- Allowed type tags: decision, preference, fact, commitment, task, todo, commitment-unresolved, milestone, lesson, relationship, project",
74
85
  "- i >= 0.80 for structural/persistent observations (major decisions, blockers, releases, commitments)",
75
86
  "- i 0.40-0.79 for potentially important observations (notable context, preferences, milestones)",
76
87
  "- i < 0.40 for contextual/routine observations",
77
88
  "- Confidence c reflects extraction certainty, not importance.",
78
89
  "- Preserve source tags when present (e.g., [main], [telegram-dm], [discord], [telegram-group]).",
79
90
  "",
91
+ "TASK EXTRACTION (required):",
92
+ `- Emit [todo] for explicit TODO phrasing: "TODO:", "we need to", "don't forget", "remember to", "make sure to".`,
93
+ `- Emit [task] for commitments/action intent: "I'll", "I will", "let me", "going to", "plan to", "should".`,
94
+ '- Emit [commitment-unresolved] for unresolved commitments/questions: "need to figure out", "TBD", "to be determined".',
95
+ '- Deadline language ("by Friday", "before the demo", "deadline is") should increase importance and usually map to [task] unless unresolved.',
96
+ "",
80
97
  "QUALITY FILTERS (important):",
81
98
  "- DO NOT observe: CLI errors, command failures, tool output parsing issues, retry attempts, debug logs.",
82
99
  " These are transient noise, not memories. Only observe errors if they represent a BLOCKER or an unresolved problem.",
@@ -236,6 +253,7 @@ ${cleaned}`;
236
253
  let importance = record.importance;
237
254
  let confidence = record.confidence;
238
255
  let type = record.type;
256
+ const inferredTaskType = this.inferTaskType(record.content);
239
257
  if (this.isCriticalContent(record.content)) {
240
258
  importance = Math.max(importance, 0.85);
241
259
  confidence = Math.max(confidence, 0.85);
@@ -246,6 +264,11 @@ ${cleaned}`;
246
264
  importance = Math.max(importance, 0.5);
247
265
  confidence = Math.max(confidence, 0.75);
248
266
  }
267
+ if (inferredTaskType) {
268
+ type = type === "fact" || type === "commitment" ? inferredTaskType : type;
269
+ importance = Math.max(importance, inferredTaskType === "commitment-unresolved" ? 0.72 : 0.65);
270
+ confidence = Math.max(confidence, 0.8);
271
+ }
249
272
  if (type === "decision" || type === "commitment" || type === "milestone") {
250
273
  importance = Math.max(importance, 0.6);
251
274
  }
@@ -340,16 +363,21 @@ ${cleaned}`;
340
363
  return renderObservationMarkdown(sections);
341
364
  }
342
365
  inferImportance(text, type) {
366
+ const inferredTaskType = this.inferTaskType(text);
343
367
  if (this.isCriticalContent(text)) return 0.9;
368
+ if (inferredTaskType === "commitment-unresolved") return 0.72;
369
+ if (inferredTaskType === "task" || inferredTaskType === "todo") return 0.65;
344
370
  if (this.isNotableContent(text)) return 0.6;
345
371
  if (type === "decision" || type === "commitment" || type === "milestone") return 0.55;
346
372
  if (type === "preference" || type === "lesson" || type === "relationship" || type === "project") return 0.45;
347
373
  return 0.2;
348
374
  }
349
375
  inferConfidence(text, type, importance) {
376
+ const inferredTaskType = this.inferTaskType(text);
350
377
  let confidence = 0.72;
351
378
  if (importance >= 0.8) confidence += 0.12;
352
379
  if (type === "decision" || type === "commitment" || type === "milestone") confidence += 0.06;
380
+ if (inferredTaskType) confidence += 0.06;
353
381
  if (/\b(?:decided|chose|committed|deadline|released|merged)\b/i.test(text)) {
354
382
  confidence += 0.05;
355
383
  }
@@ -361,6 +389,18 @@ ${cleaned}`;
361
389
  isNotableContent(text) {
362
390
  return NOTABLE_RE.test(text);
363
391
  }
392
+ inferTaskType(text) {
393
+ if (UNRESOLVED_COMMITMENT_RE.test(text)) {
394
+ return "commitment-unresolved";
395
+ }
396
+ if (TODO_SIGNAL_RE.test(text)) {
397
+ return "todo";
398
+ }
399
+ if (COMMITMENT_TASK_SIGNAL_RE.test(text) || DEADLINE_SIGNAL_RE.test(text)) {
400
+ return "task";
401
+ }
402
+ return null;
403
+ }
364
404
  normalizeText(text) {
365
405
  return text.replace(/\s+/g, " ").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim().slice(0, 280);
366
406
  }
@@ -557,6 +597,9 @@ var TYPE_TO_CATEGORY = {
557
597
  preference: "preferences",
558
598
  fact: "facts",
559
599
  commitment: "commitments",
600
+ task: "commitments",
601
+ todo: "commitments",
602
+ "commitment-unresolved": "commitments",
560
603
  milestone: "projects",
561
604
  lesson: "lessons",
562
605
  relationship: "people",
@@ -564,19 +607,35 @@ var TYPE_TO_CATEGORY = {
564
607
  };
565
608
  var Router = class {
566
609
  vaultPath;
567
- constructor(vaultPath) {
610
+ extractTasks;
611
+ now;
612
+ constructor(vaultPath, options = {}) {
568
613
  this.vaultPath = path.resolve(vaultPath);
614
+ this.extractTasks = options.extractTasks ?? true;
615
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
569
616
  }
570
617
  /**
571
618
  * Takes observation markdown and routes items to appropriate vault categories.
572
619
  * Routes only items with importance >= 0.4.
573
620
  * Returns a summary of what was routed where.
574
621
  */
575
- route(observationMarkdown) {
622
+ route(observationMarkdown, context = {}) {
576
623
  const items = this.parseObservations(observationMarkdown);
577
624
  const routed = [];
625
+ const knownWorkItems = this.extractTasks ? this.loadExistingWorkItems() : [];
626
+ let dedupHits = 0;
578
627
  for (const item of items) {
579
628
  if (item.importance < 0.4) continue;
629
+ if (this.extractTasks && this.isTaskObservation(item.type)) {
630
+ const taskResult = this.routeTaskObservation(item, context, knownWorkItems);
631
+ if (taskResult.routedItem) {
632
+ routed.push(taskResult.routedItem);
633
+ }
634
+ if (taskResult.dedupHit) {
635
+ dedupHits += 1;
636
+ }
637
+ continue;
638
+ }
580
639
  const category = this.categorize(item.type, item.content);
581
640
  if (!category) continue;
582
641
  const routedItem = {
@@ -591,9 +650,202 @@ var Router = class {
591
650
  routed.push(routedItem);
592
651
  this.appendToCategory(category, routedItem);
593
652
  }
594
- const summary = this.buildSummary(routed);
653
+ const summary = this.buildSummary(routed, dedupHits);
595
654
  return { routed, summary };
596
655
  }
656
+ isTaskObservation(type) {
657
+ return type === "task" || type === "todo" || type === "commitment-unresolved";
658
+ }
659
+ routeTaskObservation(item, context, knownWorkItems) {
660
+ const title = this.deriveTaskTitle(item.content, item.type);
661
+ if (!title) {
662
+ return { routedItem: null, dedupHit: false };
663
+ }
664
+ const duplicate = this.findDuplicateWorkItem(title, knownWorkItems);
665
+ if (duplicate) {
666
+ if (item.type === "commitment-unresolved" && this.isOpenWorkItem(duplicate)) {
667
+ this.touchExistingWorkItem(duplicate);
668
+ }
669
+ console.log(`[observer] dedup hit for task candidate: "${title}"`);
670
+ return { routedItem: null, dedupHit: true };
671
+ }
672
+ const tags = this.mergeTags(
673
+ ["open", "observer"],
674
+ item.type === "task" ? ["task"] : [],
675
+ item.type === "todo" ? ["todo"] : [],
676
+ item.type === "commitment-unresolved" ? ["commitment"] : []
677
+ );
678
+ const content = this.buildTaskContextContent(item, context);
679
+ let backlogItem;
680
+ try {
681
+ backlogItem = createBacklogItem(this.vaultPath, title, {
682
+ source: "observer",
683
+ content,
684
+ tags
685
+ });
686
+ } catch (error) {
687
+ if (error instanceof Error && /already exists/i.test(error.message)) {
688
+ console.log(`[observer] dedup hit for task candidate: "${title}"`);
689
+ return { routedItem: null, dedupHit: true };
690
+ }
691
+ throw error;
692
+ }
693
+ knownWorkItems.push({
694
+ kind: "backlog",
695
+ slug: backlogItem.slug,
696
+ title: backlogItem.title,
697
+ status: "open",
698
+ source: backlogItem.frontmatter.source,
699
+ tags: backlogItem.frontmatter.tags ?? []
700
+ });
701
+ return {
702
+ dedupHit: false,
703
+ routedItem: {
704
+ category: "backlog",
705
+ title: backlogItem.title,
706
+ content: item.content,
707
+ type: item.type,
708
+ confidence: item.confidence,
709
+ importance: item.importance,
710
+ date: item.date
711
+ }
712
+ };
713
+ }
714
+ loadExistingWorkItems() {
715
+ const taskItems = listTasks(this.vaultPath).map((task) => ({
716
+ kind: "task",
717
+ slug: task.slug,
718
+ title: task.title,
719
+ status: task.frontmatter.status,
720
+ source: task.frontmatter.source,
721
+ tags: task.frontmatter.tags ?? []
722
+ }));
723
+ const backlogItems = listBacklogItems(this.vaultPath).map((item) => ({
724
+ kind: "backlog",
725
+ slug: item.slug,
726
+ title: item.title,
727
+ status: item.frontmatter.tags?.includes("done") ? "done" : "open",
728
+ source: item.frontmatter.source,
729
+ tags: item.frontmatter.tags ?? []
730
+ }));
731
+ return [...taskItems, ...backlogItems];
732
+ }
733
+ findDuplicateWorkItem(title, knownWorkItems) {
734
+ const normalizedTitle = this.normalizeTaskTitle(title);
735
+ if (!normalizedTitle) {
736
+ return null;
737
+ }
738
+ for (const item of knownWorkItems) {
739
+ const normalizedExisting = this.normalizeTaskTitle(item.title);
740
+ if (!normalizedExisting) {
741
+ continue;
742
+ }
743
+ if (normalizedExisting === normalizedTitle) {
744
+ return item;
745
+ }
746
+ if (this.jaccardWordSimilarity(normalizedTitle, normalizedExisting) > 0.8) {
747
+ return item;
748
+ }
749
+ }
750
+ return null;
751
+ }
752
+ normalizeTaskTitle(title) {
753
+ return title.toLowerCase().replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim().slice(0, 50);
754
+ }
755
+ jaccardWordSimilarity(a, b) {
756
+ const aWords = new Set(a.split(" ").filter(Boolean));
757
+ const bWords = new Set(b.split(" ").filter(Boolean));
758
+ if (aWords.size === 0 || bWords.size === 0) {
759
+ return 0;
760
+ }
761
+ let intersection = 0;
762
+ for (const word of aWords) {
763
+ if (bWords.has(word)) {
764
+ intersection += 1;
765
+ }
766
+ }
767
+ const unionSize = aWords.size + bWords.size - intersection;
768
+ return unionSize === 0 ? 0 : intersection / unionSize;
769
+ }
770
+ deriveTaskTitle(content, type) {
771
+ let title = content.replace(/^\d{2}:\d{2}\s+/, "").replace(/\[[^\]]+\]\s*/g, "").trim();
772
+ if (type === "todo") {
773
+ title = title.replace(
774
+ /^(?:todo:\s*|we need to\s+|don't forget(?: to)?\s+|remember to\s+|make sure to\s+)/i,
775
+ ""
776
+ );
777
+ } else if (type === "task") {
778
+ title = title.replace(
779
+ /^(?:i'?ll\s+|i will\s+|let me\s+|(?:i'?m\s+)?going to\s+|plan to\s+|should\s+)/i,
780
+ ""
781
+ );
782
+ } else if (type === "commitment-unresolved") {
783
+ title = title.replace(/^(?:need to figure out\s+|tbd[:\s-]*|to be determined[:\s-]*)/i, "");
784
+ }
785
+ title = title.replace(/\s+/g, " ").replace(/^[^a-zA-Z0-9]+/, "").replace(/[.?!:;,]+$/, "").trim();
786
+ return title.slice(0, 120);
787
+ }
788
+ buildTaskContextContent(item, context) {
789
+ const lines = ["Auto-extracted by observer from session transcript."];
790
+ if (context.sessionKey) {
791
+ lines.push(`Session: ${context.sessionKey}`);
792
+ }
793
+ if (context.transcriptId) {
794
+ lines.push(`Transcript: ${context.transcriptId}`);
795
+ }
796
+ if (context.source) {
797
+ lines.push(`Source: ${context.source}`);
798
+ }
799
+ const approximateTimestamp = this.extractApproximateTimestamp(item.date, item.content, context.timestamp);
800
+ lines.push(`Approximate timestamp: ${approximateTimestamp}`);
801
+ lines.push(`Observation type: ${item.type}`);
802
+ lines.push(`Original observation: ${item.content}`);
803
+ return lines.join("\n");
804
+ }
805
+ extractApproximateTimestamp(date, content, timestamp) {
806
+ if (timestamp) {
807
+ return timestamp.toISOString();
808
+ }
809
+ const timeMatch = content.match(/\b([01]\d|2[0-3]):([0-5]\d)\b/);
810
+ if (timeMatch) {
811
+ return `${date} ${timeMatch[0]}`;
812
+ }
813
+ return date;
814
+ }
815
+ isOpenWorkItem(item) {
816
+ if (item.kind === "task") {
817
+ return item.status !== "done";
818
+ }
819
+ return item.status !== "done";
820
+ }
821
+ touchExistingWorkItem(item) {
822
+ if (item.kind === "task") {
823
+ if (!this.isOpenWorkItem(item)) {
824
+ return;
825
+ }
826
+ updateTask(this.vaultPath, item.slug, {});
827
+ return;
828
+ }
829
+ const nextTags = this.mergeTags(item.tags, ["commitment"]);
830
+ updateBacklogItem(this.vaultPath, item.slug, {
831
+ source: item.source ?? "observer",
832
+ tags: nextTags,
833
+ lastSeen: this.now().toISOString()
834
+ });
835
+ item.tags = nextTags;
836
+ }
837
+ mergeTags(...groups) {
838
+ const merged = /* @__PURE__ */ new Set();
839
+ for (const group of groups) {
840
+ for (const tag of group) {
841
+ const normalized = tag.trim().toLowerCase();
842
+ if (normalized) {
843
+ merged.add(normalized);
844
+ }
845
+ }
846
+ }
847
+ return [...merged];
848
+ }
597
849
  parseObservations(markdown) {
598
850
  const records = parseObservationMarkdown(markdown);
599
851
  return records.map((record) => ({
@@ -881,14 +1133,20 @@ ${entry}
881
1133
  for (const bg of setA) if (setB.has(bg)) intersection++;
882
1134
  return intersection / (setA.size + setB.size - intersection);
883
1135
  }
884
- buildSummary(routed) {
885
- if (routed.length === 0) return "No items routed to vault categories.";
1136
+ buildSummary(routed, dedupHits) {
1137
+ if (routed.length === 0) {
1138
+ if (dedupHits > 0) {
1139
+ return `No items routed to vault categories (dedup hits: ${dedupHits}).`;
1140
+ }
1141
+ return "No items routed to vault categories.";
1142
+ }
886
1143
  const byCat = /* @__PURE__ */ new Map();
887
1144
  for (const item of routed) {
888
1145
  byCat.set(item.category, (byCat.get(item.category) ?? 0) + 1);
889
1146
  }
890
1147
  const parts = [...byCat.entries()].map(([cat, count]) => `${cat}: ${count}`);
891
- return `Routed ${routed.length} observations \u2192 ${parts.join(", ")}`;
1148
+ const suffix = dedupHits > 0 ? ` (dedup hits: ${dedupHits})` : "";
1149
+ return `Routed ${routed.length} observations \u2192 ${parts.join(", ")}${suffix}`;
892
1150
  }
893
1151
  };
894
1152
 
@@ -905,6 +1163,7 @@ var Observer = class {
905
1163
  rawCapture;
906
1164
  router;
907
1165
  pendingMessages = [];
1166
+ pendingRouteContext = {};
908
1167
  observationsCache = "";
909
1168
  lastRoutingSummary = "";
910
1169
  constructor(vaultPath, options = {}) {
@@ -915,7 +1174,10 @@ var Observer = class {
915
1174
  this.compressor = options.compressor ?? new Compressor({ model: options.model, now: this.now });
916
1175
  this.reflector = options.reflector ?? new Reflector({ now: this.now });
917
1176
  this.rawCapture = options.rawCapture ?? true;
918
- this.router = new Router(vaultPath);
1177
+ this.router = new Router(vaultPath, {
1178
+ extractTasks: options.extractTasks,
1179
+ now: this.now
1180
+ });
919
1181
  ensureLedgerStructure(this.vaultPath);
920
1182
  this.observationsCache = this.readTodayObservations();
921
1183
  }
@@ -928,6 +1190,7 @@ var Observer = class {
928
1190
  this.persistRawMessages(incoming, options);
929
1191
  }
930
1192
  this.pendingMessages.push(...incoming);
1193
+ this.pendingRouteContext = this.mergeRouteContext(this.pendingRouteContext, options);
931
1194
  const buffered = this.pendingMessages.join("\n");
932
1195
  if (this.estimateTokens(buffered) < this.tokenThreshold) {
933
1196
  return;
@@ -940,14 +1203,16 @@ var Observer = class {
940
1203
  this.writeObservationFile(todayPath, existing);
941
1204
  }
942
1205
  const compressedRaw = (await this.compressor.compress(this.pendingMessages, existing)).trim();
1206
+ const routeContext = this.pendingRouteContext;
943
1207
  this.pendingMessages = [];
1208
+ this.pendingRouteContext = {};
944
1209
  const compressed = this.deduplicateObservationMarkdown(compressedRaw);
945
1210
  if (!compressed) {
946
1211
  return;
947
1212
  }
948
1213
  this.writeObservationFile(todayPath, compressed);
949
1214
  this.observationsCache = compressed;
950
- const { summary } = this.router.route(compressed);
1215
+ const { summary } = this.router.route(compressed, routeContext);
951
1216
  if (summary) {
952
1217
  this.lastRoutingSummary = summary;
953
1218
  }
@@ -968,12 +1233,14 @@ var Observer = class {
968
1233
  this.writeObservationFile(todayPath, existing);
969
1234
  }
970
1235
  const compressedRaw = (await this.compressor.compress(this.pendingMessages, existing)).trim();
1236
+ const routeContext = this.pendingRouteContext;
971
1237
  this.pendingMessages = [];
1238
+ this.pendingRouteContext = {};
972
1239
  const compressed = this.deduplicateObservationMarkdown(compressedRaw);
973
1240
  if (compressed) {
974
1241
  this.writeObservationFile(todayPath, compressed);
975
1242
  this.observationsCache = compressed;
976
- const { summary } = this.router.route(compressed);
1243
+ const { summary } = this.router.route(compressed, routeContext);
977
1244
  this.lastRoutingSummary = summary;
978
1245
  }
979
1246
  return { observations: this.observationsCache, routingSummary: this.lastRoutingSummary };
@@ -1062,6 +1329,14 @@ var Observer = class {
1062
1329
  }
1063
1330
  return "openclaw";
1064
1331
  }
1332
+ mergeRouteContext(existing, incoming) {
1333
+ const merged = { ...existing };
1334
+ if (incoming.source) merged.source = incoming.source;
1335
+ if (incoming.sessionKey) merged.sessionKey = incoming.sessionKey;
1336
+ if (incoming.transcriptId) merged.transcriptId = incoming.transcriptId;
1337
+ if (incoming.timestamp) merged.timestamp = incoming.timestamp;
1338
+ return merged;
1339
+ }
1065
1340
  };
1066
1341
 
1067
1342
  export {