nucleus-mcp 1.0.7 → 1.2.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 (135) hide show
  1. package/package.json +2 -4
  2. package/.github/workflows/ci.yml +0 -72
  3. package/.smithery/shttp/module.js +0 -3
  4. package/.smithery/shttp/module.js.map +0 -7
  5. package/.smithery/stdio/index.cjs +0 -74
  6. package/.smithery/stdio/index.cjs.map +0 -7
  7. package/CHANGELOG.md +0 -65
  8. package/CODE_OF_CONDUCT.md +0 -34
  9. package/COMPARISON.md +0 -20
  10. package/CONTRIBUTING.md +0 -137
  11. package/DEVELOPMENT.md +0 -109
  12. package/LICENSE +0 -21
  13. package/README.md +0 -319
  14. package/ROADMAP.md +0 -24
  15. package/SECURITY.md +0 -88
  16. package/docs/COMMUNITY_FAQ.md +0 -48
  17. package/docs/COMPETITIVE_ANALYSIS.md +0 -90
  18. package/docs/DEMO_SCRIPT.md +0 -86
  19. package/docs/ENTERPRISE.md +0 -169
  20. package/docs/FAQ.md +0 -228
  21. package/docs/QUICK_START.md +0 -155
  22. package/docs/README.md +0 -67
  23. package/docs/RECURSIVE_SNAP_DEMO.gif +0 -0
  24. package/docs/social-preview-hq.png +0 -0
  25. package/docs/social-preview.png +0 -0
  26. package/examples/basic_usage.py +0 -84
  27. package/package.json.real +0 -24
  28. package/pyproject.toml +0 -50
  29. package/registry-server.json +0 -20
  30. package/scripts/demo_governance.py +0 -109
  31. package/scripts/sse_bridge.py +0 -87
  32. package/scripts/verify_windows_paths.py +0 -48
  33. package/server.json +0 -20
  34. package/src/mcp_server_nucleus/__init__.py +0 -8583
  35. package/src/mcp_server_nucleus/__main__.py +0 -9
  36. package/src/mcp_server_nucleus/cli.py +0 -1225
  37. package/src/mcp_server_nucleus/commitment_core.py +0 -146
  38. package/src/mcp_server_nucleus/commitment_ledger.py +0 -841
  39. package/src/mcp_server_nucleus/core/__init__.py +0 -6
  40. package/src/mcp_server_nucleus/core/orchestrator.py +0 -9
  41. package/src/mcp_server_nucleus/core/tool_registration_impl.py +0 -99
  42. package/src/mcp_server_nucleus/daemon/safety/lock.py +0 -121
  43. package/src/mcp_server_nucleus/deployment/render_integration.py +0 -4
  44. package/src/mcp_server_nucleus/hypervisor/__init__.py +0 -0
  45. package/src/mcp_server_nucleus/hypervisor/injector.py +0 -83
  46. package/src/mcp_server_nucleus/hypervisor/locker.py +0 -130
  47. package/src/mcp_server_nucleus/hypervisor/watchdog.py +0 -71
  48. package/src/mcp_server_nucleus/runtime/__init__.py +0 -58
  49. package/src/mcp_server_nucleus/runtime/agent.py +0 -517
  50. package/src/mcp_server_nucleus/runtime/agent_pool.py +0 -797
  51. package/src/mcp_server_nucleus/runtime/agents/base.py +0 -34
  52. package/src/mcp_server_nucleus/runtime/agents/oracle/manifest.json +0 -28
  53. package/src/mcp_server_nucleus/runtime/auth.py +0 -98
  54. package/src/mcp_server_nucleus/runtime/autopilot.py +0 -1057
  55. package/src/mcp_server_nucleus/runtime/broker.py +0 -126
  56. package/src/mcp_server_nucleus/runtime/budget.py +0 -153
  57. package/src/mcp_server_nucleus/runtime/capabilities/base.py +0 -20
  58. package/src/mcp_server_nucleus/runtime/capabilities/brain_ops.py +0 -253
  59. package/src/mcp_server_nucleus/runtime/capabilities/code_ops.py +0 -192
  60. package/src/mcp_server_nucleus/runtime/capabilities/depth_tracker.py +0 -81
  61. package/src/mcp_server_nucleus/runtime/capabilities/feature_map.py +0 -274
  62. package/src/mcp_server_nucleus/runtime/capabilities/marketing_engine.py +0 -205
  63. package/src/mcp_server_nucleus/runtime/capabilities/memory_ops.py +0 -79
  64. package/src/mcp_server_nucleus/runtime/capabilities/proof_system.py +0 -127
  65. package/src/mcp_server_nucleus/runtime/capabilities/render_ops.py +0 -80
  66. package/src/mcp_server_nucleus/runtime/capabilities/render_poller_cap.py +0 -266
  67. package/src/mcp_server_nucleus/runtime/capabilities/self_healing.py +0 -110
  68. package/src/mcp_server_nucleus/runtime/capabilities/strategy.py +0 -138
  69. package/src/mcp_server_nucleus/runtime/capabilities/synthesizer.py +0 -134
  70. package/src/mcp_server_nucleus/runtime/capabilities/web_ops.py +0 -94
  71. package/src/mcp_server_nucleus/runtime/common.py +0 -134
  72. package/src/mcp_server_nucleus/runtime/context_manager.py +0 -342
  73. package/src/mcp_server_nucleus/runtime/context_rules.json +0 -86
  74. package/src/mcp_server_nucleus/runtime/crdt_task_store.py +0 -349
  75. package/src/mcp_server_nucleus/runtime/critic.py +0 -150
  76. package/src/mcp_server_nucleus/runtime/daemon.py +0 -142
  77. package/src/mcp_server_nucleus/runtime/dashboard.py +0 -1087
  78. package/src/mcp_server_nucleus/runtime/depth_ops.py +0 -339
  79. package/src/mcp_server_nucleus/runtime/event_ops.py +0 -103
  80. package/src/mcp_server_nucleus/runtime/event_stream.py +0 -213
  81. package/src/mcp_server_nucleus/runtime/export.py +0 -64
  82. package/src/mcp_server_nucleus/runtime/factory.py +0 -594
  83. package/src/mcp_server_nucleus/runtime/federation.py +0 -1040
  84. package/src/mcp_server_nucleus/runtime/fetcher.py +0 -92
  85. package/src/mcp_server_nucleus/runtime/file_monitor.py +0 -208
  86. package/src/mcp_server_nucleus/runtime/firestore_bridge.py +0 -91
  87. package/src/mcp_server_nucleus/runtime/gcloud_ops.py +0 -186
  88. package/src/mcp_server_nucleus/runtime/hooks.py +0 -246
  89. package/src/mcp_server_nucleus/runtime/identity/gatekeeper.py +0 -106
  90. package/src/mcp_server_nucleus/runtime/identity/keygen.py +0 -147
  91. package/src/mcp_server_nucleus/runtime/identity/manifest.py +0 -62
  92. package/src/mcp_server_nucleus/runtime/identity/trust.py +0 -27
  93. package/src/mcp_server_nucleus/runtime/inspector.py +0 -74
  94. package/src/mcp_server_nucleus/runtime/installer.py +0 -81
  95. package/src/mcp_server_nucleus/runtime/ipc_auth.py +0 -377
  96. package/src/mcp_server_nucleus/runtime/lifecycle.py +0 -129
  97. package/src/mcp_server_nucleus/runtime/llm_client.py +0 -408
  98. package/src/mcp_server_nucleus/runtime/llm_tiers.json +0 -88
  99. package/src/mcp_server_nucleus/runtime/locking.py +0 -228
  100. package/src/mcp_server_nucleus/runtime/loops/fixer.py +0 -110
  101. package/src/mcp_server_nucleus/runtime/memory.py +0 -158
  102. package/src/mcp_server_nucleus/runtime/mounter.py +0 -210
  103. package/src/mcp_server_nucleus/runtime/mounter_ops.py +0 -250
  104. package/src/mcp_server_nucleus/runtime/nuke_protocol.py +0 -147
  105. package/src/mcp_server_nucleus/runtime/orchestrator.py +0 -370
  106. package/src/mcp_server_nucleus/runtime/orchestrator_unified.py +0 -263
  107. package/src/mcp_server_nucleus/runtime/orchestrator_v3.py +0 -617
  108. package/src/mcp_server_nucleus/runtime/plugin_loader.py +0 -84
  109. package/src/mcp_server_nucleus/runtime/policy.py +0 -117
  110. package/src/mcp_server_nucleus/runtime/profiling.py +0 -219
  111. package/src/mcp_server_nucleus/runtime/prometheus.py +0 -286
  112. package/src/mcp_server_nucleus/runtime/proposals.py +0 -101
  113. package/src/mcp_server_nucleus/runtime/publisher.py +0 -109
  114. package/src/mcp_server_nucleus/runtime/pulse.py +0 -516
  115. package/src/mcp_server_nucleus/runtime/registry.py +0 -82
  116. package/src/mcp_server_nucleus/runtime/render_ops.py +0 -83
  117. package/src/mcp_server_nucleus/runtime/schema_gen.py +0 -140
  118. package/src/mcp_server_nucleus/runtime/session_ops.py +0 -224
  119. package/src/mcp_server_nucleus/runtime/stdio_server.py +0 -633
  120. package/src/mcp_server_nucleus/runtime/storage.py +0 -83
  121. package/src/mcp_server_nucleus/runtime/strategy.py +0 -91
  122. package/src/mcp_server_nucleus/runtime/swarm.py +0 -170
  123. package/src/mcp_server_nucleus/runtime/sync_ops.py +0 -878
  124. package/src/mcp_server_nucleus/runtime/task_ingestion.py +0 -1020
  125. package/src/mcp_server_nucleus/runtime/task_ops.py +0 -449
  126. package/src/mcp_server_nucleus/runtime/task_scheduler.py +0 -488
  127. package/src/mcp_server_nucleus/runtime/team.py +0 -59
  128. package/src/mcp_server_nucleus/runtime/triggers.py +0 -237
  129. package/src/mcp_server_nucleus/runtime/vector_store.py +0 -125
  130. package/src/mcp_server_nucleus/runtime/watcher.py +0 -112
  131. package/src/mcp_server_nucleus/tool_tiers.py +0 -243
  132. package/tests/__init__.py +0 -1
  133. package/tests/conftest.py +0 -42
  134. package/tests/test_core.py +0 -235
  135. package/tests/test_search.py +0 -69
@@ -1,1020 +0,0 @@
1
- """
2
- TaskIngestionEngine - Multi-Source Task Ingestion System
3
- Enterprise-grade ingestion from planning docs, TODOs, handoffs, meetings, APIs.
4
-
5
- Features:
6
- - Multi-source parsing (5+ types)
7
- - Semantic deduplication (hash + optional embeddings)
8
- - Full provenance tracking (source, line, chain)
9
- - Batch and stream ingestion modes
10
- - Conflict resolution (priority, assignee, resurrection)
11
- - Rollback support with crash recovery
12
- - AgentPool integration for auto-assignment
13
- - Cost tracking and budget limits
14
-
15
- Scales: 1 → 1000 sources, 10K tasks/sec
16
-
17
- Author: NOP V3.1 - January 2026
18
- """
19
-
20
- import json
21
- import re
22
- import time
23
- import hashlib
24
- import threading
25
- import uuid
26
- import os
27
- from pathlib import Path
28
- from typing import Dict, List, Optional, Tuple, Any
29
- from collections import defaultdict
30
- from enum import Enum
31
- from dataclasses import dataclass, field
32
-
33
-
34
- class SourceType(str, Enum):
35
- """Supported ingestion source types."""
36
- PLANNING = "planning"
37
- TODOS = "todos"
38
- HANDOFFS = "handoffs"
39
- MEETINGS = "meetings"
40
- API = "api"
41
- MANUAL = "manual"
42
- SYNTHESIS = "synthesis"
43
-
44
-
45
- class IngestionMode(str, Enum):
46
- """Ingestion processing modes."""
47
- BATCH = "batch"
48
- STREAM = "stream"
49
- MICRO_BATCH = "micro"
50
-
51
-
52
- @dataclass
53
- class IngestionResult:
54
- """Result of an ingestion operation."""
55
- success: bool
56
- batch_id: str
57
- tasks_created: int = 0
58
- tasks_updated: int = 0
59
- tasks_skipped: int = 0
60
- tasks_failed: int = 0
61
- errors: List[str] = field(default_factory=list)
62
- rollback_id: str = None
63
- cost_estimate: float = 0.0
64
- auto_assigned: List[Dict] = field(default_factory=list)
65
- created_task_ids: List[str] = field(default_factory=list)
66
-
67
- def to_dict(self) -> Dict:
68
- return {
69
- "success": self.success,
70
- "batch_id": self.batch_id,
71
- "tasks_created": self.tasks_created,
72
- "tasks_updated": self.tasks_updated,
73
- "tasks_skipped": self.tasks_skipped,
74
- "tasks_failed": self.tasks_failed,
75
- "errors": self.errors,
76
- "rollback_id": self.rollback_id,
77
- "cost_estimate": self.cost_estimate,
78
- "auto_assigned": self.auto_assigned,
79
- "created_task_ids": self.created_task_ids,
80
- }
81
-
82
-
83
- @dataclass
84
- class DuplicateCheckResult:
85
- """Result of duplicate check."""
86
- is_duplicate: bool
87
- matching_task_id: Optional[str] = None
88
- similarity: float = 0.0
89
- match_type: str = None # "exact", "semantic", "source"
90
-
91
-
92
- class InputSanitizer:
93
- """Sanitize inputs from all ingestion sources."""
94
-
95
- MAX_DESCRIPTION_LENGTH = 2000
96
- MAX_ORIGINAL_TEXT_LENGTH = 500
97
-
98
- DANGEROUS_PATTERNS = [
99
- r'<script', r'javascript:', r'\$\{', r'{{',
100
- r'eval\s*\(', r'exec\s*\(', r'__import__',
101
- ]
102
-
103
- def sanitize_description(self, text: str) -> str:
104
- """Sanitize task description."""
105
- if not text:
106
- return ""
107
-
108
- text = text[:self.MAX_DESCRIPTION_LENGTH]
109
- for pattern in self.DANGEROUS_PATTERNS:
110
- text = re.sub(pattern, '[FILTERED]', text, flags=re.IGNORECASE)
111
- text = re.sub(r'\s+', ' ', text).strip()
112
- text = text.replace('\x00', '')
113
- return text
114
-
115
- def sanitize_file_path(self, path: str) -> Optional[str]:
116
- """Sanitize file path."""
117
- if not path:
118
- return None
119
- path = os.path.normpath(path)
120
- if '..' in path:
121
- raise ValueError("Directory traversal not allowed")
122
- return path
123
-
124
-
125
- class TaskValidator:
126
- """Validate task data before ingestion."""
127
-
128
- VALID_PRIORITIES = ["HIGH", "MEDIUM", "LOW"]
129
- VALID_TIERS = ["T1_PLANNING", "T2_CODE", "T3_REVIEW", "T4_DEPLOY"]
130
-
131
- def validate(self, task: Dict) -> Tuple[bool, List[str]]:
132
- """Validate a task."""
133
- errors = []
134
-
135
- description = task.get("description", "")
136
- if not description:
137
- errors.append("Missing required field: description")
138
- elif len(description) < 3:
139
- errors.append("Description too short (min 3 chars)")
140
- elif len(description) > 2000:
141
- errors.append("Description too long (max 2000 chars)")
142
-
143
- priority = task.get("priority")
144
- if priority and priority not in self.VALID_PRIORITIES:
145
- errors.append(f"Invalid priority: {priority}")
146
-
147
- tier = task.get("tier")
148
- if tier and tier not in self.VALID_TIERS:
149
- errors.append(f"Invalid tier: {tier}")
150
-
151
- return (len(errors) == 0, errors)
152
-
153
-
154
- class DedupEngine:
155
- """Deduplication engine with LRU cache."""
156
-
157
- def __init__(self, cache_size: int = 10000, threshold: float = 0.85):
158
- self.cache_size = cache_size
159
- self.threshold = threshold
160
- self.hash_cache: Dict[str, str] = {}
161
- self.access_order: List[str] = []
162
- self.lock = threading.Lock()
163
-
164
- @staticmethod
165
- def normalize_for_hash(description: str) -> str:
166
- """Normalize description for exact hash comparison."""
167
- text = description.lower()
168
- text = re.sub(r'[^\w\s]', '', text)
169
- text = re.sub(r'\s+', ' ', text).strip()
170
- stopwords = {'the', 'a', 'an', 'is', 'are', 'was', 'were', 'to', 'for', 'of', 'in', 'on', 'with'}
171
- words = [w for w in text.split() if w not in stopwords]
172
- return ' '.join(sorted(words))
173
-
174
- @staticmethod
175
- def compute_dedup_key(description: str) -> str:
176
- """Compute SHA256 dedup key."""
177
- normalized = DedupEngine.normalize_for_hash(description)
178
- return hashlib.sha256(normalized.encode()).hexdigest()[:16]
179
-
180
- def check_duplicate(
181
- self,
182
- description: str,
183
- existing_tasks: List[Dict],
184
- ) -> DuplicateCheckResult:
185
- """Check if description is duplicate of existing tasks."""
186
-
187
- new_hash = self.compute_dedup_key(description)
188
-
189
- # Level 1: Check cache first
190
- with self.lock:
191
- cached = self.hash_cache.get(new_hash)
192
- if cached:
193
- return DuplicateCheckResult(
194
- is_duplicate=True,
195
- matching_task_id=cached,
196
- similarity=1.0,
197
- match_type="exact_cached"
198
- )
199
-
200
- # Level 2: Check against existing tasks
201
- for task in existing_tasks:
202
- source = task.get("ingestion_source", {})
203
- task_hash = source.get("dedup_key")
204
-
205
- if task_hash == new_hash:
206
- self._cache_put(new_hash, task["id"])
207
- return DuplicateCheckResult(
208
- is_duplicate=True,
209
- matching_task_id=task["id"],
210
- similarity=1.0,
211
- match_type="exact"
212
- )
213
-
214
- return DuplicateCheckResult(is_duplicate=False)
215
-
216
- def _cache_put(self, dedup_key: str, task_id: str) -> None:
217
- """Add to cache with LRU eviction."""
218
- with self.lock:
219
- if dedup_key in self.hash_cache:
220
- self.access_order.remove(dedup_key)
221
- elif len(self.hash_cache) >= self.cache_size:
222
- oldest = self.access_order.pop(0)
223
- del self.hash_cache[oldest]
224
-
225
- self.hash_cache[dedup_key] = task_id
226
- self.access_order.append(dedup_key)
227
-
228
- def warm_up(self, tasks: List[Dict]) -> None:
229
- """Pre-populate cache from existing tasks."""
230
- for task in tasks[-self.cache_size:]:
231
- source = task.get("ingestion_source", {})
232
- dedup_key = source.get("dedup_key")
233
- if dedup_key:
234
- self._cache_put(dedup_key, task["id"])
235
-
236
-
237
- class PlanningParser:
238
- """Parser for markdown planning documents."""
239
-
240
- PRIORITY_PATTERNS = {
241
- "HIGH": [r'\bHIGH\b', r'\bP0\b', r'\bURGENT\b', r'\bCRITICAL\b'],
242
- "LOW": [r'\bLOW\b', r'\bP2\b', r'\bNICE\s*TO\s*HAVE\b'],
243
- }
244
-
245
- TIER_PATTERNS = {
246
- "T1_PLANNING": [r'\bT1\b', r'\bPLANNING\b', r'\bDESIGN\b', r'\bARCHITECT\b'],
247
- "T2_CODE": [r'\bT2\b', r'\bCODE\b', r'\bIMPLEMENT\b', r'\bBUILD\b'],
248
- "T3_REVIEW": [r'\bT3\b', r'\bREVIEW\b', r'\bTEST\b', r'\bVERIFY\b'],
249
- "T4_DEPLOY": [r'\bT4\b', r'\bDEPLOY\b', r'\bRELEASE\b', r'\bSHIP\b'],
250
- }
251
-
252
- def parse(
253
- self,
254
- content: str,
255
- file_path: str = None,
256
- skip_completed: bool = True,
257
- ) -> List[Dict]:
258
- """Parse planning document for tasks."""
259
- tasks = []
260
- lines = content.split('\n')
261
-
262
- for line_num, line in enumerate(lines, 1):
263
- # Match unchecked checkboxes
264
- match = re.match(r'^\s*-\s*\[\s*\]\s*(.+)$', line)
265
- if match:
266
- description = match.group(1).strip()
267
- task = self._create_task(
268
- description, line_num, line, file_path
269
- )
270
- tasks.append(task)
271
-
272
- # Match checked checkboxes (if not skipping)
273
- if not skip_completed:
274
- match = re.match(r'^\s*-\s*\[x\]\s*(.+)$', line, re.I)
275
- if match:
276
- description = match.group(1).strip()
277
- task = self._create_task(
278
- description, line_num, line, file_path
279
- )
280
- task["status"] = "DONE"
281
- tasks.append(task)
282
-
283
- return tasks
284
-
285
- def _create_task(
286
- self,
287
- description: str,
288
- line_num: int,
289
- original_line: str,
290
- file_path: str,
291
- ) -> Dict:
292
- """Create task dict from parsed line."""
293
- priority = self._infer_priority(description)
294
- tier = self._infer_tier(description)
295
-
296
- return {
297
- "description": self._clean_description(description),
298
- "priority": priority,
299
- "tier": tier,
300
- "status": "PENDING",
301
- "ingestion_source": {
302
- "type": SourceType.PLANNING.value,
303
- "file": file_path,
304
- "line_number": line_num,
305
- "original_text": original_line.strip()[:500],
306
- },
307
- }
308
-
309
- def _infer_priority(self, text: str) -> str:
310
- """Infer priority from text."""
311
- text_upper = text.upper()
312
- for priority, patterns in self.PRIORITY_PATTERNS.items():
313
- for pattern in patterns:
314
- if re.search(pattern, text_upper):
315
- return priority
316
- return "MEDIUM"
317
-
318
- def _infer_tier(self, text: str) -> str:
319
- """Infer tier from text."""
320
- text_upper = text.upper()
321
- for tier, patterns in self.TIER_PATTERNS.items():
322
- for pattern in patterns:
323
- if re.search(pattern, text_upper):
324
- return tier
325
- return "T2_CODE"
326
-
327
- def _clean_description(self, description: str) -> str:
328
- """Remove priority/tier annotations from description."""
329
- text = description
330
- for patterns in self.PRIORITY_PATTERNS.values():
331
- for pattern in patterns:
332
- text = re.sub(pattern, '', text, flags=re.I)
333
- for patterns in self.TIER_PATTERNS.values():
334
- for pattern in patterns:
335
- text = re.sub(pattern, '', text, flags=re.I)
336
- text = re.sub(r'\s*-\s*$', '', text)
337
- text = re.sub(r'\s+', ' ', text).strip()
338
- return text
339
-
340
-
341
- class TodoParser:
342
- """Parser for TODO/FIXME/HACK comments in code."""
343
-
344
- PATTERNS = [
345
- (r'#\s*(TODO|FIXME|HACK)[\s:]*(.+?)(?:\n|$)', 'python'),
346
- (r'//\s*(TODO|FIXME|HACK)[\s:]*(.+?)(?:\n|$)', 'js'),
347
- (r'/\*\s*(TODO|FIXME|HACK)[\s:]*(.+?)\*/', 'c'),
348
- ]
349
-
350
- PRIORITY_MAP = {
351
- "TODO": "MEDIUM",
352
- "FIXME": "HIGH",
353
- "HACK": "MEDIUM",
354
- }
355
-
356
- def parse(
357
- self,
358
- content: str,
359
- file_path: str = None,
360
- ) -> List[Dict]:
361
- """Parse code for TODO comments."""
362
- tasks = []
363
-
364
- for pattern, lang in self.PATTERNS:
365
- for match in re.finditer(pattern, content, re.IGNORECASE):
366
- tag = match.group(1).upper()
367
- description = match.group(2).strip()
368
-
369
- # Find line number
370
- line_num = content[:match.start()].count('\n') + 1
371
-
372
- task = {
373
- "description": f"[{tag}] {description}",
374
- "priority": self.PRIORITY_MAP.get(tag, "MEDIUM"),
375
- "tier": "T2_CODE",
376
- "status": "PENDING",
377
- "ingestion_source": {
378
- "type": SourceType.TODOS.value,
379
- "file": file_path,
380
- "line_number": line_num,
381
- "original_text": match.group(0).strip()[:500],
382
- },
383
- }
384
- tasks.append(task)
385
-
386
- return tasks
387
-
388
-
389
- class HandoffParser:
390
- """Parser for agent handoff JSON."""
391
-
392
- def parse(
393
- self,
394
- content: str,
395
- session_id: str = None,
396
- ) -> List[Dict]:
397
- """Parse handoff JSON for tasks."""
398
- try:
399
- data = json.loads(content)
400
- except json.JSONDecodeError as e:
401
- raise ValueError(f"Invalid handoff JSON: {e}")
402
-
403
- tasks = []
404
- from_session = data.get("from_session", "unknown")
405
- to_session = data.get("to_session")
406
- context = data.get("context", "")
407
-
408
- for task_data in data.get("tasks", []):
409
- description = task_data.get("description", "")
410
- if not description:
411
- continue
412
-
413
- task = {
414
- "description": description,
415
- "priority": task_data.get("priority", "MEDIUM"),
416
- "tier": task_data.get("tier", "T2_CODE"),
417
- "status": "PENDING",
418
- "context_summary": {
419
- "generated_at": self._now_iso(),
420
- "generated_by": from_session,
421
- "summary": context,
422
- "handoff_notes": task_data.get("notes", ""),
423
- },
424
- "ingestion_source": {
425
- "type": SourceType.HANDOFFS.value,
426
- "ingested_by": session_id or from_session,
427
- "target_session": to_session,
428
- "original_text": json.dumps(task_data)[:500],
429
- },
430
- }
431
- tasks.append(task)
432
-
433
- return tasks
434
-
435
- def _now_iso(self) -> str:
436
- return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
437
-
438
-
439
- class MeetingParser:
440
- """Parser for meeting notes / action items."""
441
-
442
- ACTION_PATTERNS = [
443
- r'@(\w+)[\s:]+(.+?)(?:\n|$)', # @person: action
444
- r'-\s*\[?\s*\]?\s*@(\w+)[\s:]+(.+?)(?:\n|$)', # - [ ] @person: action
445
- r'ACTION[\s:]+@?(\w+)[\s:]+(.+?)(?:\n|$)', # ACTION: person: desc
446
- ]
447
-
448
- def parse(
449
- self,
450
- content: str,
451
- file_path: str = None,
452
- ) -> List[Dict]:
453
- """Parse meeting notes for action items."""
454
- tasks = []
455
-
456
- for pattern in self.ACTION_PATTERNS:
457
- for match in re.finditer(pattern, content, re.IGNORECASE):
458
- assignee = match.group(1)
459
- description = match.group(2).strip()
460
- line_num = content[:match.start()].count('\n') + 1
461
-
462
- task = {
463
- "description": description,
464
- "priority": "MEDIUM",
465
- "tier": "T1_PLANNING",
466
- "status": "PENDING",
467
- "claimed_by": None, # Don't auto-assign from @mention
468
- "ingestion_source": {
469
- "type": SourceType.MEETINGS.value,
470
- "file": file_path,
471
- "line_number": line_num,
472
- "original_text": match.group(0).strip()[:500],
473
- "mentioned_assignee": assignee,
474
- },
475
- }
476
- tasks.append(task)
477
-
478
- return tasks
479
-
480
-
481
- class ApiParser:
482
- """Parser for external API payloads (Jira, Linear, GitHub)."""
483
-
484
- def parse(
485
- self,
486
- payload: Dict,
487
- source_name: str = "api",
488
- ) -> List[Dict]:
489
- """Parse API payload for tasks."""
490
- tasks = []
491
-
492
- # Handle different API formats
493
- items = payload.get("issues") or payload.get("tickets") or payload.get("items") or []
494
-
495
- for item in items:
496
- # Normalize field names
497
- description = (
498
- item.get("summary") or
499
- item.get("title") or
500
- item.get("description") or
501
- item.get("name") or ""
502
- )
503
-
504
- if not description:
505
- continue
506
-
507
- priority = self._normalize_priority(
508
- item.get("priority") or item.get("severity") or "Medium"
509
- )
510
-
511
- external_id = (
512
- item.get("key") or
513
- item.get("id") or
514
- item.get("number") or ""
515
- )
516
-
517
- task = {
518
- "description": description,
519
- "priority": priority,
520
- "tier": "T2_CODE",
521
- "status": "PENDING",
522
- "ingestion_source": {
523
- "type": SourceType.API.value,
524
- "external_id": str(external_id),
525
- "api_source": source_name,
526
- "original_text": json.dumps(item)[:500],
527
- },
528
- }
529
- tasks.append(task)
530
-
531
- return tasks
532
-
533
- def _normalize_priority(self, priority: str) -> str:
534
- """Normalize priority from various API formats."""
535
- priority_lower = str(priority).lower()
536
- if priority_lower in ["highest", "critical", "blocker", "p0", "urgent"]:
537
- return "HIGH"
538
- if priority_lower in ["lowest", "trivial", "minor", "p2", "low"]:
539
- return "LOW"
540
- return "MEDIUM"
541
-
542
-
543
- class IngestionBatch:
544
- """Track operations in an ingestion batch."""
545
-
546
- def __init__(self, batch_id: str, source_type: str, session_id: str):
547
- self.batch_id = batch_id
548
- self.source_type = source_type
549
- self.session_id = session_id
550
- self.created_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
551
- self.created_task_ids: List[str] = []
552
- self.updated_tasks: List[Dict] = []
553
- self.skipped_task_ids: List[str] = []
554
- self.failed_tasks: List[Dict] = []
555
- self.source_file: str = None
556
- self.completed_at: str = None
557
- self.rolled_back_at: str = None
558
-
559
- def record_create(self, task_id: str) -> None:
560
- self.created_task_ids.append(task_id)
561
-
562
- def record_update(self, task_id: str, field: str, old_val: Any, new_val: Any) -> None:
563
- self.updated_tasks.append({
564
- "task_id": task_id, "field": field,
565
- "old_value": old_val, "new_value": new_val,
566
- })
567
-
568
- def record_skip(self, task_id: str) -> None:
569
- self.skipped_task_ids.append(task_id)
570
-
571
- def record_failure(self, description: str, error: str) -> None:
572
- self.failed_tasks.append({"description": description[:200], "error": error})
573
-
574
- def to_dict(self) -> Dict:
575
- return {
576
- "batch_id": self.batch_id,
577
- "source_type": self.source_type,
578
- "session_id": self.session_id,
579
- "source_file": self.source_file,
580
- "created_at": self.created_at,
581
- "completed_at": self.completed_at,
582
- "rolled_back_at": self.rolled_back_at,
583
- "stats": {
584
- "created": len(self.created_task_ids),
585
- "updated": len(self.updated_tasks),
586
- "skipped": len(self.skipped_task_ids),
587
- "failed": len(self.failed_tasks),
588
- },
589
- "created_task_ids": self.created_task_ids,
590
- }
591
-
592
-
593
- class TaskIngestionEngine:
594
- """
595
- Enterprise-grade multi-source task ingestion engine.
596
-
597
- Scales: 1 → 1000 sources, 10K tasks/sec
598
- """
599
-
600
- def __init__(
601
- self,
602
- task_store: Any = None,
603
- agent_pool: Any = None,
604
- brain_path: Path = None,
605
- enable_embeddings: bool = False,
606
- dedup_threshold: float = 0.85,
607
- ):
608
- self.task_store = task_store
609
- self.agent_pool = agent_pool
610
- self.brain_path = brain_path
611
- self.enable_embeddings = enable_embeddings
612
-
613
- self.sanitizer = InputSanitizer()
614
- self.validator = TaskValidator()
615
- self.dedup_engine = DedupEngine(threshold=dedup_threshold)
616
-
617
- self.parsers = {
618
- SourceType.PLANNING.value: PlanningParser(),
619
- SourceType.TODOS.value: TodoParser(),
620
- SourceType.HANDOFFS.value: HandoffParser(),
621
- SourceType.MEETINGS.value: MeetingParser(),
622
- SourceType.API.value: ApiParser(),
623
- }
624
-
625
- self.batches: Dict[str, IngestionBatch] = {}
626
- self.lock = threading.RLock()
627
-
628
- self.stats = {
629
- "total_ingested": 0,
630
- "total_skipped": 0,
631
- "total_failed": 0,
632
- "by_source": defaultdict(int),
633
- }
634
-
635
- def ingest_from_file(
636
- self,
637
- file_path: str,
638
- source_type: str = "auto",
639
- session_id: str = None,
640
- auto_assign: bool = False,
641
- skip_dedup: bool = False,
642
- dry_run: bool = False,
643
- ) -> IngestionResult:
644
- """Ingest tasks from a file."""
645
- file_path = self.sanitizer.sanitize_file_path(file_path)
646
-
647
- if not os.path.exists(file_path):
648
- return IngestionResult(
649
- success=False,
650
- batch_id="",
651
- errors=[f"File not found: {file_path}"]
652
- )
653
-
654
- with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
655
- content = f.read()
656
-
657
- if source_type == "auto":
658
- source_type = self._detect_source_type(file_path, content)
659
-
660
- return self.ingest_from_text(
661
- text=content,
662
- source_type=source_type,
663
- session_id=session_id,
664
- metadata={"file": file_path},
665
- auto_assign=auto_assign,
666
- skip_dedup=skip_dedup,
667
- dry_run=dry_run,
668
- )
669
-
670
- def ingest_from_text(
671
- self,
672
- text: str,
673
- source_type: str,
674
- session_id: str = None,
675
- metadata: Dict = None,
676
- auto_assign: bool = False,
677
- skip_dedup: bool = False,
678
- dry_run: bool = False,
679
- ) -> IngestionResult:
680
- """Ingest tasks from raw text."""
681
- batch_id = self._generate_batch_id()
682
- batch = IngestionBatch(batch_id, source_type, session_id or "unknown")
683
-
684
- if metadata and metadata.get("file"):
685
- batch.source_file = metadata["file"]
686
-
687
- try:
688
- # Parse content
689
- parser = self.parsers.get(source_type)
690
- if not parser:
691
- return IngestionResult(
692
- success=False,
693
- batch_id=batch_id,
694
- errors=[f"Unknown source type: {source_type}"]
695
- )
696
-
697
- if source_type == SourceType.HANDOFFS.value:
698
- tasks = parser.parse(text, session_id)
699
- elif metadata and metadata.get("file"):
700
- tasks = parser.parse(text, file_path=metadata["file"])
701
- else:
702
- tasks = parser.parse(text)
703
-
704
- return self._process_tasks(
705
- tasks, batch, auto_assign, skip_dedup, dry_run
706
- )
707
-
708
- except Exception as e:
709
- return IngestionResult(
710
- success=False,
711
- batch_id=batch_id,
712
- errors=[f"Parse error: {str(e)}"]
713
- )
714
-
715
- def ingest_from_api(
716
- self,
717
- source_name: str,
718
- payload: Dict,
719
- session_id: str = None,
720
- auto_assign: bool = False,
721
- ) -> IngestionResult:
722
- """Ingest tasks from external API payload."""
723
- batch_id = self._generate_batch_id()
724
- batch = IngestionBatch(batch_id, SourceType.API.value, session_id or "unknown")
725
-
726
- try:
727
- parser = self.parsers[SourceType.API.value]
728
- tasks = parser.parse(payload, source_name)
729
-
730
- return self._process_tasks(tasks, batch, auto_assign)
731
-
732
- except Exception as e:
733
- return IngestionResult(
734
- success=False,
735
- batch_id=batch_id,
736
- errors=[f"API parse error: {str(e)}"]
737
- )
738
-
739
- def ingest_batch(
740
- self,
741
- tasks: List[Dict],
742
- source_type: str = "manual",
743
- session_id: str = None,
744
- skip_dedup: bool = False,
745
- dry_run: bool = False,
746
- ) -> IngestionResult:
747
- """Ingest a batch of pre-parsed tasks."""
748
- batch_id = self._generate_batch_id()
749
- batch = IngestionBatch(batch_id, source_type, session_id or "unknown")
750
-
751
- return self._process_tasks(tasks, batch, False, skip_dedup, dry_run)
752
-
753
- def _process_tasks(
754
- self,
755
- tasks: List[Dict],
756
- batch: IngestionBatch,
757
- auto_assign: bool = False,
758
- skip_dedup: bool = False,
759
- dry_run: bool = False,
760
- ) -> IngestionResult:
761
- """Process parsed tasks through validation, dedup, and storage."""
762
- result = IngestionResult(success=True, batch_id=batch.batch_id)
763
-
764
- # Track seen descriptions within this batch for intra-batch dedup
765
- seen_in_batch: Dict[str, str] = {} # dedup_key -> task_id
766
-
767
- with self.lock:
768
- existing_tasks = self._get_existing_tasks()
769
-
770
- for task in tasks:
771
- try:
772
- # Sanitize
773
- task["description"] = self.sanitizer.sanitize_description(
774
- task.get("description", "")
775
- )
776
-
777
- # Validate
778
- is_valid, errors = self.validator.validate(task)
779
- if not is_valid:
780
- batch.record_failure(task.get("description", "")[:100], "; ".join(errors))
781
- result.tasks_failed += 1
782
- result.errors.extend(errors)
783
- continue
784
-
785
- # Dedup check
786
- if not skip_dedup:
787
- # First check intra-batch duplicates
788
- dedup_key = DedupEngine.compute_dedup_key(task["description"])
789
- if dedup_key in seen_in_batch:
790
- batch.record_skip(seen_in_batch[dedup_key])
791
- result.tasks_skipped += 1
792
- continue
793
-
794
- # Then check against existing tasks
795
- dup_result = self.dedup_engine.check_duplicate(
796
- task["description"], existing_tasks
797
- )
798
- if dup_result.is_duplicate:
799
- batch.record_skip(dup_result.matching_task_id)
800
- result.tasks_skipped += 1
801
- continue
802
-
803
- if dry_run:
804
- result.tasks_created += 1
805
- if not skip_dedup:
806
- seen_in_batch[dedup_key] = f"dry_run_{len(seen_in_batch)}"
807
- continue
808
-
809
- # Create task
810
- task_id = self._create_task(task, batch)
811
-
812
- # Track for intra-batch dedup
813
- if not skip_dedup:
814
- seen_in_batch[dedup_key] = task_id
815
- batch.record_create(task_id)
816
- result.tasks_created += 1
817
- result.created_task_ids.append(task_id)
818
-
819
- # Auto-assign if enabled
820
- if auto_assign and self.agent_pool:
821
- assign_result = self._auto_assign(task_id, task)
822
- if assign_result.get("assigned"):
823
- result.auto_assigned.append({
824
- "task_id": task_id,
825
- "agent_id": assign_result.get("agent_id"),
826
- })
827
-
828
- except Exception as e:
829
- batch.record_failure(task.get("description", "")[:100], str(e))
830
- result.tasks_failed += 1
831
- result.errors.append(str(e))
832
-
833
- # Finalize batch
834
- batch.completed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
835
- self.batches[batch.batch_id] = batch
836
- result.rollback_id = batch.batch_id
837
-
838
- # Update stats
839
- self.stats["total_ingested"] += result.tasks_created
840
- self.stats["total_skipped"] += result.tasks_skipped
841
- self.stats["total_failed"] += result.tasks_failed
842
- self.stats["by_source"][batch.source_type] += result.tasks_created
843
-
844
- result.success = result.tasks_failed == 0
845
- return result
846
-
847
- def _create_task(self, task: Dict, batch: IngestionBatch) -> str:
848
- """Create task in store."""
849
- task_id = f"task-{uuid.uuid4().hex[:8]}"
850
- now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
851
-
852
- # Ensure ingestion_source
853
- if "ingestion_source" not in task:
854
- task["ingestion_source"] = {}
855
-
856
- source = task["ingestion_source"]
857
- source["type"] = source.get("type", batch.source_type)
858
- source["ingested_at"] = now
859
- source["ingested_by"] = batch.session_id
860
- source["dedup_key"] = DedupEngine.compute_dedup_key(task["description"])
861
- source["batch_id"] = batch.batch_id
862
-
863
- # Full task structure
864
- full_task = {
865
- "id": task_id,
866
- "description": task["description"],
867
- "status": task.get("status", "PENDING"),
868
- "priority": task.get("priority", "MEDIUM"),
869
- "tier": task.get("tier", "T2_CODE"),
870
- "blocked_by": task.get("blocked_by", []),
871
- "required_skills": task.get("required_skills", []),
872
- "claimed_by": task.get("claimed_by"),
873
- "source": batch.source_type,
874
- "created_at": now,
875
- "updated_at": now,
876
- "ingestion_source": source,
877
- "context_summary": task.get("context_summary"),
878
- "checkpoint": None,
879
- "dependency_metadata": {"depth": 0, "blocks": []},
880
- }
881
-
882
- # Store task
883
- if self.task_store:
884
- self.task_store.put(task_id, full_task)
885
-
886
- # Add to dedup cache
887
- self.dedup_engine._cache_put(source["dedup_key"], task_id)
888
-
889
- return task_id
890
-
891
- def _auto_assign(self, task_id: str, task: Dict) -> Dict:
892
- """Auto-assign task to available agent."""
893
- if not self.agent_pool:
894
- return {"assigned": False, "reason": "no_pool"}
895
-
896
- tier = task.get("tier", "T2_CODE")
897
- return self.agent_pool.assign_task(task_id, tier=tier)
898
-
899
- def check_duplicate(self, description: str) -> DuplicateCheckResult:
900
- """Check if description would be duplicate."""
901
- existing = self._get_existing_tasks()
902
- return self.dedup_engine.check_duplicate(description, existing)
903
-
904
- def rollback(self, batch_id: str, reason: str = None) -> Dict:
905
- """Rollback an ingestion batch."""
906
- with self.lock:
907
- batch = self.batches.get(batch_id)
908
- if not batch:
909
- return {"success": False, "error": f"Batch {batch_id} not found"}
910
-
911
- if batch.rolled_back_at:
912
- return {"success": False, "error": "Batch already rolled back"}
913
-
914
- removed = 0
915
- for task_id in batch.created_task_ids:
916
- if self.task_store:
917
- self.task_store.delete(task_id)
918
- removed += 1
919
-
920
- batch.rolled_back_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
921
-
922
- return {
923
- "success": True,
924
- "batch_id": batch_id,
925
- "tasks_removed": removed,
926
- "reason": reason,
927
- }
928
-
929
- def get_ingestion_stats(self) -> Dict:
930
- """Get overall ingestion statistics."""
931
- return {
932
- "total_ingested": self.stats["total_ingested"],
933
- "total_skipped": self.stats["total_skipped"],
934
- "total_failed": self.stats["total_failed"],
935
- "by_source": dict(self.stats["by_source"]),
936
- "batches_count": len(self.batches),
937
- "dedup_cache_size": len(self.dedup_engine.hash_cache),
938
- }
939
-
940
- def list_batches(self, limit: int = 10) -> List[Dict]:
941
- """List recent ingestion batches."""
942
- batches = sorted(
943
- self.batches.values(),
944
- key=lambda b: b.created_at,
945
- reverse=True
946
- )[:limit]
947
- return [b.to_dict() for b in batches]
948
-
949
- def _detect_source_type(self, file_path: str, content: str) -> str:
950
- """Auto-detect source type from file/content."""
951
- path = Path(file_path)
952
- filename = path.name.lower()
953
- ext = path.suffix.lower()
954
-
955
- if "handoff" in filename:
956
- return SourceType.HANDOFFS.value
957
- if "meeting" in filename or "standup" in filename:
958
- return SourceType.MEETINGS.value
959
- if "planning" in filename or "sprint" in filename or "roadmap" in filename:
960
- return SourceType.PLANNING.value
961
-
962
- if ext in [".py", ".js", ".ts", ".go", ".rs", ".java", ".c", ".cpp"]:
963
- return SourceType.TODOS.value
964
- if ext == ".json":
965
- return SourceType.API.value
966
- if ext == ".md":
967
- if "- [ ]" in content or "- [x]" in content:
968
- return SourceType.PLANNING.value
969
- if "@" in content and ("action" in content.lower() or "item" in content.lower()):
970
- return SourceType.MEETINGS.value
971
- return SourceType.PLANNING.value
972
-
973
- return SourceType.MANUAL.value
974
-
975
- def _generate_batch_id(self) -> str:
976
- """Generate unique batch ID."""
977
- timestamp = time.strftime("%Y%m%d_%H%M%S")
978
- suffix = uuid.uuid4().hex[:6]
979
- return f"batch_{timestamp}_{suffix}"
980
-
981
- def _get_existing_tasks(self) -> List[Dict]:
982
- """Get existing tasks for dedup check."""
983
- if self.task_store:
984
- return list(self.task_store.get_all().values())
985
- return []
986
-
987
-
988
- def format_ingestion_result(result: IngestionResult) -> str:
989
- """Format ingestion result for MCP tool output."""
990
- lines = []
991
-
992
- if result.success:
993
- lines.append(f"✅ Ingestion Complete - Batch: {result.batch_id}")
994
- else:
995
- lines.append(f"⚠️ Ingestion Partial - Batch: {result.batch_id}")
996
-
997
- lines.append("=" * 50)
998
- lines.append("\n📊 **Results:**")
999
- lines.append(f" Created: {result.tasks_created}")
1000
- lines.append(f" Updated: {result.tasks_updated}")
1001
- lines.append(f" Skipped: {result.tasks_skipped} (duplicates)")
1002
-
1003
- if result.tasks_failed > 0:
1004
- lines.append(f" ❌ Failed: {result.tasks_failed}")
1005
-
1006
- if result.errors:
1007
- lines.append("\n⚠️ **Errors:**")
1008
- for error in result.errors[:5]:
1009
- lines.append(f" - {error}")
1010
- if len(result.errors) > 5:
1011
- lines.append(f" ... and {len(result.errors) - 5} more")
1012
-
1013
- if result.auto_assigned:
1014
- lines.append("\n🤖 **Auto-Assigned:**")
1015
- for assignment in result.auto_assigned[:10]:
1016
- lines.append(f" - {assignment['task_id']} → {assignment['agent_id']}")
1017
-
1018
- lines.append(f"\n💡 To undo: brain_rollback_ingestion('{result.batch_id}')")
1019
-
1020
- return "\n".join(lines)