dr-claw 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1380 -0
- package/NOTICE +17 -0
- package/README.md +875 -0
- package/README.zh-CN.md +857 -0
- package/dist/api-docs.html +878 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/_basePickBy-C0n1uo4u.js +1 -0
- package/dist/assets/_baseUniq-BRNmhFkc.js +1 -0
- package/dist/assets/arc-DOMBmC_A.js +1 -0
- package/dist/assets/architectureDiagram-VXUJARFQ-CtMWglbD.js +36 -0
- package/dist/assets/blockDiagram-VD42YOAC-DK_gTkJy.js +122 -0
- package/dist/assets/c4Diagram-YG6GDRKO-DrDAvsn7.js +10 -0
- package/dist/assets/channel-BFOE7OyC.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-CGbhdZcN.js +1 -0
- package/dist/assets/chunk-55IACEB6-sGeFSxen.js +1 -0
- package/dist/assets/chunk-B4BG7PRW-RxgHAU_P.js +165 -0
- package/dist/assets/chunk-DI55MBZ5-CvHUXZrx.js +220 -0
- package/dist/assets/chunk-FMBD7UC4-Sn7cXVNU.js +15 -0
- package/dist/assets/chunk-QN33PNHL-bDKOHppN.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-B9kN5LGm.js +1 -0
- package/dist/assets/chunk-TZMSLE5B-CCBe6len.js +1 -0
- package/dist/assets/classDiagram-2ON5EDUG-DQoMNYlY.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-DQoMNYlY.js +1 -0
- package/dist/assets/clone-BulQSblH.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-r6sLiHBW.js +1 -0
- package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
- package/dist/assets/dagre-6UL2VRFP-AbsaR1Oa.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-PSM6KHXK-DjCvdUyy.js +24 -0
- package/dist/assets/diagram-QEK2KX5R-CqiocXkH.js +43 -0
- package/dist/assets/diagram-S2PKOQOG-D5ALeoII.js +24 -0
- package/dist/assets/erDiagram-Q2GNP2WA-C1AeOsiz.js +60 -0
- package/dist/assets/flowDiagram-NV44I4VS-C98ve_DO.js +162 -0
- package/dist/assets/ganttDiagram-JELNMOA3-3zdLvcxD.js +267 -0
- package/dist/assets/gitGraphDiagram-V2S2FVAM-nDuDMsHh.js +65 -0
- package/dist/assets/graph-Blm-emkR.js +1 -0
- package/dist/assets/index-B8Yutsnm.css +1 -0
- package/dist/assets/index-Cf6gyhc3.js +654 -0
- package/dist/assets/infoDiagram-HS3SLOUP-CmzE3q0R.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/journeyDiagram-XKPGCS4Q-CYaW7LrP.js +139 -0
- package/dist/assets/kanban-definition-3W4ZIXB7-mkmW7p0j.js +89 -0
- package/dist/assets/layout-Ct2kFIId.js +1 -0
- package/dist/assets/linear-CKG9EwIJ.js +1 -0
- package/dist/assets/mermaid.core-CaKcUnnd.js +250 -0
- package/dist/assets/mindmap-definition-VGOIOE7T-Bct-pYin.js +68 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-ADFJNKIX-C8wFD4Jr.js +30 -0
- package/dist/assets/quadrantDiagram-AYHSOK5B-CuiE_f46.js +7 -0
- package/dist/assets/requirementDiagram-UZGBJVZJ-C4v1CqU8.js +64 -0
- package/dist/assets/sankeyDiagram-TZEHDZUN-keAqqs-Y.js +10 -0
- package/dist/assets/sequenceDiagram-WL72ISMW-ClNbvKTB.js +145 -0
- package/dist/assets/stateDiagram-FKZM4ZOC-D8We-2vq.js +1 -0
- package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +1 -0
- package/dist/assets/timeline-definition-IT6M3QCI-DsvEMT5T.js +61 -0
- package/dist/assets/treemap-GDKQZRPO-TJrzN7Bj.js +162 -0
- package/dist/assets/vendor-codemirror-ansM-eig.js +33 -0
- package/dist/assets/vendor-react-BWF6YQQ3.js +8 -0
- package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
- package/dist/assets/xychartDiagram-PRI3JC2R-BdATmY5F.js +7 -0
- package/dist/clear-cache.html +85 -0
- package/dist/convert-icons.md +53 -0
- package/dist/dr-claw.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/generate-icons.js +49 -0
- package/dist/icons/claude-ai-icon.svg +1 -0
- package/dist/icons/codex-white.svg +3 -0
- package/dist/icons/codex.svg +3 -0
- package/dist/icons/cursor-white.svg +12 -0
- package/dist/icons/cursor.svg +1 -0
- package/dist/icons/file.svg +16997 -0
- package/dist/icons/generate-icons.md +19 -0
- package/dist/icons/icon-128x128.png +0 -0
- package/dist/icons/icon-144x144.png +0 -0
- package/dist/icons/icon-152x152.png +0 -0
- package/dist/icons/icon-192x192.png +0 -0
- package/dist/icons/icon-384x384.png +0 -0
- package/dist/icons/icon-512x512.png +0 -0
- package/dist/icons/icon-72x72.png +0 -0
- package/dist/icons/icon-96x96.png +0 -0
- package/dist/icons/news/arxiv.svg +1 -0
- package/dist/icons/news/huggingface.svg +8 -0
- package/dist/icons/news/x-black.png +0 -0
- package/dist/icons/news/x-white.png +0 -0
- package/dist/icons/news/xiaohongshu.png +0 -0
- package/dist/index.html +51 -0
- package/dist/leveraged-cognition.png +0 -0
- package/dist/manifest.json +61 -0
- package/dist/screenshots/chat.png +0 -0
- package/dist/screenshots/new_claw.png +0 -0
- package/dist/screenshots/news_dashboard.png +0 -0
- package/dist/screenshots/project_dashboard.png +0 -0
- package/dist/screenshots/research_lab.png +0 -0
- package/dist/screenshots/skill_library.png +0 -0
- package/dist/screenshots/task_list.png +0 -0
- package/dist/screenshots/terminal_example.png +0 -0
- package/dist/screenshots/terminal_example1.png +0 -0
- package/dist/sw.js +58 -0
- package/dist/wechat-group-qr.jpg +0 -0
- package/package.json +133 -0
- package/public/api-docs.html +878 -0
- package/public/clear-cache.html +85 -0
- package/public/convert-icons.md +53 -0
- package/public/dr-claw.png +0 -0
- package/public/favicon.png +0 -0
- package/public/generate-icons.js +49 -0
- package/public/icons/claude-ai-icon.svg +1 -0
- package/public/icons/codex-white.svg +3 -0
- package/public/icons/codex.svg +3 -0
- package/public/icons/cursor-white.svg +12 -0
- package/public/icons/cursor.svg +1 -0
- package/public/icons/file.svg +16997 -0
- package/public/icons/generate-icons.md +19 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/icons/news/arxiv.svg +1 -0
- package/public/icons/news/huggingface.svg +8 -0
- package/public/icons/news/x-black.png +0 -0
- package/public/icons/news/x-white.png +0 -0
- package/public/icons/news/xiaohongshu.png +0 -0
- package/public/leveraged-cognition.png +0 -0
- package/public/manifest.json +61 -0
- package/public/screenshots/chat.png +0 -0
- package/public/screenshots/new_claw.png +0 -0
- package/public/screenshots/news_dashboard.png +0 -0
- package/public/screenshots/project_dashboard.png +0 -0
- package/public/screenshots/research_lab.png +0 -0
- package/public/screenshots/skill_library.png +0 -0
- package/public/screenshots/task_list.png +0 -0
- package/public/screenshots/terminal_example.png +0 -0
- package/public/screenshots/terminal_example1.png +0 -0
- package/public/sw.js +58 -0
- package/public/wechat-group-qr.jpg +0 -0
- package/scripts/export-skills-catalog-v2.mjs +746 -0
- package/scripts/fix-node-pty.js +45 -0
- package/scripts/gemini_image_core.py +470 -0
- package/scripts/install-python-tools.js +64 -0
- package/server/AGENTS.md +73 -0
- package/server/__tests__/gemini-legacy-thought.test.mjs +88 -0
- package/server/__tests__/runtimePorts.test.mjs +115 -0
- package/server/__tests__/session-delete.test.mjs +166 -0
- package/server/bin/compute-node +70 -0
- package/server/claude-sdk.js +799 -0
- package/server/cli-chat.js +488 -0
- package/server/cli.js +354 -0
- package/server/compute-node.js +439 -0
- package/server/constants/config.js +5 -0
- package/server/cursor-cli.js +340 -0
- package/server/database/auth.db +0 -0
- package/server/database/db.js +1852 -0
- package/server/database/init.sql +194 -0
- package/server/gemini-cli.js +1352 -0
- package/server/index.js +3154 -0
- package/server/load-env.js +71 -0
- package/server/local-gpu.js +860 -0
- package/server/middleware/auth.js +100 -0
- package/server/openai-codex.js +724 -0
- package/server/openrouter.js +890 -0
- package/server/project-token-usage.js +407 -0
- package/server/projects.js +3973 -0
- package/server/routes/agent.js +1272 -0
- package/server/routes/auth.js +128 -0
- package/server/routes/auto-research.js +558 -0
- package/server/routes/cli-auth.js +958 -0
- package/server/routes/codex.js +344 -0
- package/server/routes/commands.js +601 -0
- package/server/routes/compute.js +358 -0
- package/server/routes/cursor.js +795 -0
- package/server/routes/git.js +1348 -0
- package/server/routes/mcp-utils.js +48 -0
- package/server/routes/mcp.js +618 -0
- package/server/routes/news.js +711 -0
- package/server/routes/projects.js +633 -0
- package/server/routes/references.js +338 -0
- package/server/routes/settings.js +285 -0
- package/server/routes/skills.js +914 -0
- package/server/routes/taskmaster.js +2566 -0
- package/server/routes/telemetry.js +89 -0
- package/server/routes/user.js +155 -0
- package/server/scripts/fix_session_names.js +57 -0
- package/server/scripts/research-news/common_words.py +19 -0
- package/server/scripts/research-news/link_keywords.py +298 -0
- package/server/scripts/research-news/scan_existing_notes.py +266 -0
- package/server/scripts/research-news/scoring_utils.py +227 -0
- package/server/scripts/research-news/search_arxiv.py +842 -0
- package/server/scripts/research-news/search_huggingface.py +438 -0
- package/server/scripts/research-news/search_x.py +490 -0
- package/server/scripts/research-news/search_xiaohongshu.py +610 -0
- package/server/taskmaster-templates/README.md +150 -0
- package/server/taskmaster-templates/README.zh-CN.md +150 -0
- package/server/taskmaster-templates/ai-research-dataset.json +230 -0
- package/server/taskmaster-templates/ai-research-method-model.json +230 -0
- package/server/taskmaster-templates/ai-research-position-paper.json +230 -0
- package/server/telemetry.js +121 -0
- package/server/templates/AGENTS.md +103 -0
- package/server/templates/CLAUDE.md +104 -0
- package/server/templates/GEMINI.md +103 -0
- package/server/templates/cursor-project.md +108 -0
- package/server/templates/index.js +37 -0
- package/server/utils/__tests__/sessionTokenUsage.test.js +82 -0
- package/server/utils/auto-research-mailer.js +57 -0
- package/server/utils/cliResolution.js +170 -0
- package/server/utils/commandParser.js +303 -0
- package/server/utils/cursorCommand.js +61 -0
- package/server/utils/geminiApiKey.js +28 -0
- package/server/utils/gitConfig.js +24 -0
- package/server/utils/imageAttachmentFiles.js +35 -0
- package/server/utils/mcp-detector.js +198 -0
- package/server/utils/parsers/bibtex-parser.js +207 -0
- package/server/utils/permissions.js +100 -0
- package/server/utils/runtimePorts.js +212 -0
- package/server/utils/sessionFormatting.js +68 -0
- package/server/utils/sessionIndex.js +100 -0
- package/server/utils/sessionMode.js +95 -0
- package/server/utils/sessionTokenUsage.js +105 -0
- package/server/utils/taskmaster-websocket.js +129 -0
- package/server/utils/zotero-client.js +136 -0
- package/shared/errorClassifier.js +102 -0
- package/shared/geminiThinkingSupport.d.ts +22 -0
- package/shared/geminiThinkingSupport.js +96 -0
- package/shared/geminiThoughtParser.d.ts +8 -0
- package/shared/geminiThoughtParser.js +30 -0
- package/shared/modelConstants.js +139 -0
|
@@ -0,0 +1,2566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TASKMASTER API ROUTES
|
|
3
|
+
* ====================
|
|
4
|
+
*
|
|
5
|
+
* This module provides API endpoints for TaskMaster integration including:
|
|
6
|
+
* - .pipeline folder detection in project directories
|
|
7
|
+
* - MCP server configuration detection
|
|
8
|
+
* - TaskMaster state and metadata management
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import express from 'express';
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { promises as fsPromises } from 'fs';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import { extractProjectDirectory } from '../projects.js';
|
|
17
|
+
import { detectTaskMasterMCPServer } from '../utils/mcp-detector.js';
|
|
18
|
+
import { broadcastTaskMasterProjectUpdate, broadcastTaskMasterTasksUpdate } from '../utils/taskmaster-websocket.js';
|
|
19
|
+
|
|
20
|
+
const router = express.Router();
|
|
21
|
+
const PIPELINE_DIR = '.pipeline';
|
|
22
|
+
const LEGACY_TASKMASTER_DIR = '.taskmaster';
|
|
23
|
+
const DEFAULT_TASKS_TAG = 'master';
|
|
24
|
+
const DEFAULT_RESEARCH_BRIEF_FILENAME = 'research_brief.json';
|
|
25
|
+
const DEFAULT_MAX_TASKS = 30;
|
|
26
|
+
const STAGE_ORDER = ['survey', 'ideation', 'experiment', 'publication', 'promotion'];
|
|
27
|
+
const STAGE_LABELS = {
|
|
28
|
+
survey: 'Survey',
|
|
29
|
+
ideation: 'Ideation',
|
|
30
|
+
experiment: 'Experiment',
|
|
31
|
+
publication: 'Publication',
|
|
32
|
+
promotion: 'Promotion',
|
|
33
|
+
presentation: 'Promotion',
|
|
34
|
+
};
|
|
35
|
+
const STAGE_PROMPT_HINTS = {
|
|
36
|
+
survey: 'Establish the literature baseline, collect evidence, and map open gaps before committing to a direction.',
|
|
37
|
+
ideation: 'Clarify thesis, scope boundaries, and evidence framing before execution.',
|
|
38
|
+
experiment: 'Turn assumptions into an executable protocol with measurable validation criteria.',
|
|
39
|
+
publication: 'Convert outcomes into a coherent manuscript narrative with concrete submission artifacts.',
|
|
40
|
+
promotion: 'Transform research outcomes into homepage assets, visual slides, narration scripts, and demo videos.',
|
|
41
|
+
};
|
|
42
|
+
const DEFAULT_STAGE_SKILL_MAP = {
|
|
43
|
+
survey: {
|
|
44
|
+
base: ['inno-deep-research', 'academic-researcher', 'dataset-discovery'],
|
|
45
|
+
byTaskType: {
|
|
46
|
+
exploration: ['inno-deep-research', 'academic-researcher', 'dataset-discovery'],
|
|
47
|
+
analysis: ['inno-deep-research', 'academic-researcher'],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
ideation: {
|
|
51
|
+
base: ['inno-pipeline-planner', 'inno-idea-generation', 'inno-prepare-resources'],
|
|
52
|
+
byTaskType: {
|
|
53
|
+
analysis: ['inno-idea-generation', 'academic-researcher'],
|
|
54
|
+
exploration: ['inno-idea-generation', 'inno-code-survey'],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
experiment: {
|
|
58
|
+
base: ['inno-code-survey', 'inno-experiment-dev', 'inno-experiment-analysis'],
|
|
59
|
+
byTaskType: {
|
|
60
|
+
implementation: ['inno-experiment-dev'],
|
|
61
|
+
analysis: ['inno-experiment-analysis'],
|
|
62
|
+
exploration: ['inno-code-survey'],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
publication: {
|
|
66
|
+
base: ['inno-paper-writing', 'inno-reference-audit', 'inno-rclone-to-overleaf'],
|
|
67
|
+
byTaskType: {
|
|
68
|
+
writing: ['inno-paper-writing'],
|
|
69
|
+
analysis: ['inno-reference-audit'],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
promotion: {
|
|
73
|
+
base: ['making-academic-presentations'],
|
|
74
|
+
byTaskType: {
|
|
75
|
+
scripting: ['making-academic-presentations'],
|
|
76
|
+
rendering: ['making-academic-presentations'],
|
|
77
|
+
narration: ['making-academic-presentations'],
|
|
78
|
+
delivery: ['making-academic-presentations'],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
const DEFAULT_BRIEF_SECTIONS = {
|
|
83
|
+
survey: {
|
|
84
|
+
literature_scope: '',
|
|
85
|
+
key_references: [],
|
|
86
|
+
synthesis_summary: '',
|
|
87
|
+
open_gaps: [],
|
|
88
|
+
},
|
|
89
|
+
ideation: {
|
|
90
|
+
research_goal: '',
|
|
91
|
+
problem_framing: '',
|
|
92
|
+
evidence_plan: '',
|
|
93
|
+
success_criteria: [],
|
|
94
|
+
},
|
|
95
|
+
experiment: {
|
|
96
|
+
hypothesis_or_validation_goal: '',
|
|
97
|
+
dataset_or_data_source: '',
|
|
98
|
+
method_or_protocol: '',
|
|
99
|
+
evaluation_plan: '',
|
|
100
|
+
},
|
|
101
|
+
publication: {
|
|
102
|
+
paper_outline: '',
|
|
103
|
+
figures_tables_plan: '',
|
|
104
|
+
artifact_plan: '',
|
|
105
|
+
submission_checklist: [],
|
|
106
|
+
},
|
|
107
|
+
promotion: {
|
|
108
|
+
slide_outline: '',
|
|
109
|
+
deck_style: '',
|
|
110
|
+
tts_config: '',
|
|
111
|
+
video_assembly_plan: '',
|
|
112
|
+
homepage_plan: '',
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
116
|
+
const SKILLS_DIR = path.resolve(__dirname, '..', '..', 'skills');
|
|
117
|
+
const STAGE_SKILL_MAP_PATH = path.join(SKILLS_DIR, 'stage-skill-map.json');
|
|
118
|
+
let cachedStageSkillMap = null;
|
|
119
|
+
let cachedStageSkillMapMtimeMs = null;
|
|
120
|
+
function normalizeStageSkillMap(rawMap = {}) {
|
|
121
|
+
const normalized = {};
|
|
122
|
+
STAGE_ORDER.forEach((stage) => {
|
|
123
|
+
const source = rawMap?.[stage]
|
|
124
|
+
|| (stage === 'promotion' ? rawMap?.presentation : null)
|
|
125
|
+
|| {};
|
|
126
|
+
normalized[stage] = {
|
|
127
|
+
base: Array.isArray(source.base) ? source.base.map((item) => String(item || '').trim()).filter(Boolean) : [],
|
|
128
|
+
byTaskType: source.byTaskType && typeof source.byTaskType === 'object'
|
|
129
|
+
? Object.fromEntries(
|
|
130
|
+
Object.entries(source.byTaskType).map(([taskType, skills]) => [
|
|
131
|
+
String(taskType || '').trim(),
|
|
132
|
+
Array.isArray(skills) ? skills.map((item) => String(item || '').trim()).filter(Boolean) : [],
|
|
133
|
+
]),
|
|
134
|
+
)
|
|
135
|
+
: {},
|
|
136
|
+
};
|
|
137
|
+
if (normalized[stage].base.length === 0) {
|
|
138
|
+
normalized[stage].base = DEFAULT_STAGE_SKILL_MAP[stage]?.base || [];
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return normalized;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getStageSkillMap() {
|
|
145
|
+
try {
|
|
146
|
+
const stats = fs.statSync(STAGE_SKILL_MAP_PATH);
|
|
147
|
+
if (
|
|
148
|
+
cachedStageSkillMap &&
|
|
149
|
+
typeof cachedStageSkillMapMtimeMs === 'number' &&
|
|
150
|
+
cachedStageSkillMapMtimeMs === stats.mtimeMs
|
|
151
|
+
) {
|
|
152
|
+
return cachedStageSkillMap;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const content = fs.readFileSync(STAGE_SKILL_MAP_PATH, 'utf8');
|
|
156
|
+
const parsed = JSON.parse(content);
|
|
157
|
+
cachedStageSkillMap = normalizeStageSkillMap(parsed);
|
|
158
|
+
cachedStageSkillMapMtimeMs = stats.mtimeMs;
|
|
159
|
+
return cachedStageSkillMap;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (!cachedStageSkillMap) {
|
|
162
|
+
cachedStageSkillMap = normalizeStageSkillMap(DEFAULT_STAGE_SKILL_MAP);
|
|
163
|
+
}
|
|
164
|
+
return cachedStageSkillMap;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildDefaultBriefPipeline(stageSkillMap) {
|
|
169
|
+
const map = stageSkillMap || getStageSkillMap();
|
|
170
|
+
return {
|
|
171
|
+
version: '1.1',
|
|
172
|
+
mode: 'idea',
|
|
173
|
+
stages: {
|
|
174
|
+
survey: {
|
|
175
|
+
required_elements: [
|
|
176
|
+
'sections.survey.literature_scope',
|
|
177
|
+
'sections.survey.synthesis_summary',
|
|
178
|
+
],
|
|
179
|
+
optional_elements: [
|
|
180
|
+
'sections.survey.key_references',
|
|
181
|
+
'sections.survey.open_gaps',
|
|
182
|
+
],
|
|
183
|
+
quality_gate: [
|
|
184
|
+
'The literature scope is explicit and bounded',
|
|
185
|
+
'The synthesis identifies concrete open gaps or unresolved tensions',
|
|
186
|
+
],
|
|
187
|
+
task_blueprints: [
|
|
188
|
+
{
|
|
189
|
+
id: 'survey_collect_references',
|
|
190
|
+
title: 'Collect and triage the core literature set',
|
|
191
|
+
description: 'Assemble the most relevant references, group them by theme, and note inclusion boundaries.',
|
|
192
|
+
taskType: 'exploration',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: 'survey_summarize_gaps',
|
|
196
|
+
title: 'Summarize trends, baselines, and open gaps',
|
|
197
|
+
description: 'Write a compact synthesis of what is known, what is contested, and where the project can contribute.',
|
|
198
|
+
taskType: 'analysis',
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
recommended_skills: map.survey.base,
|
|
202
|
+
},
|
|
203
|
+
ideation: {
|
|
204
|
+
required_elements: [
|
|
205
|
+
'sections.ideation.research_goal',
|
|
206
|
+
'sections.ideation.problem_framing',
|
|
207
|
+
],
|
|
208
|
+
optional_elements: [
|
|
209
|
+
'sections.ideation.evidence_plan',
|
|
210
|
+
'sections.ideation.success_criteria',
|
|
211
|
+
],
|
|
212
|
+
quality_gate: [
|
|
213
|
+
'At least one clear research direction is defined',
|
|
214
|
+
'Problem framing and expected value are specific',
|
|
215
|
+
],
|
|
216
|
+
task_blueprints: [
|
|
217
|
+
{
|
|
218
|
+
id: 'ideation_generate_candidates',
|
|
219
|
+
title: 'Generate and compare candidate research directions',
|
|
220
|
+
description: 'Produce multiple candidate directions and compare novelty, feasibility, and expected impact.',
|
|
221
|
+
taskType: 'exploration',
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 'ideation_select_direction',
|
|
225
|
+
title: 'Select one direction with explicit rationale',
|
|
226
|
+
description: 'Pick one direction and document tradeoffs and scope boundaries.',
|
|
227
|
+
taskType: 'analysis',
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
recommended_skills: map.ideation.base,
|
|
231
|
+
},
|
|
232
|
+
experiment: {
|
|
233
|
+
required_elements: [
|
|
234
|
+
'sections.experiment.hypothesis_or_validation_goal',
|
|
235
|
+
'sections.experiment.method_or_protocol',
|
|
236
|
+
'sections.experiment.evaluation_plan',
|
|
237
|
+
],
|
|
238
|
+
optional_elements: [
|
|
239
|
+
'sections.experiment.dataset_or_data_source',
|
|
240
|
+
],
|
|
241
|
+
quality_gate: [
|
|
242
|
+
'Validation goal can be measured objectively',
|
|
243
|
+
'Method and evaluation protocol are executable',
|
|
244
|
+
],
|
|
245
|
+
task_blueprints: [
|
|
246
|
+
{
|
|
247
|
+
id: 'experiment_define_protocol',
|
|
248
|
+
title: 'Define executable experiment protocol',
|
|
249
|
+
description: 'Translate method and evaluation plan into executable steps and checkpoints.',
|
|
250
|
+
taskType: 'implementation',
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
id: 'experiment_run_analysis',
|
|
254
|
+
title: 'Run baseline analysis and record outcomes',
|
|
255
|
+
description: 'Execute baseline validation and summarize key findings and gaps.',
|
|
256
|
+
taskType: 'analysis',
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
recommended_skills: map.experiment.base,
|
|
260
|
+
},
|
|
261
|
+
publication: {
|
|
262
|
+
required_elements: [
|
|
263
|
+
'sections.publication.paper_outline',
|
|
264
|
+
'sections.publication.submission_checklist',
|
|
265
|
+
],
|
|
266
|
+
optional_elements: [
|
|
267
|
+
'sections.publication.figures_tables_plan',
|
|
268
|
+
'sections.publication.artifact_plan',
|
|
269
|
+
],
|
|
270
|
+
quality_gate: [
|
|
271
|
+
'Contribution narrative and structure are coherent',
|
|
272
|
+
'Submission checklist and artifacts are complete',
|
|
273
|
+
],
|
|
274
|
+
task_blueprints: [
|
|
275
|
+
{
|
|
276
|
+
id: 'publication_outline_to_draft',
|
|
277
|
+
title: 'Expand outline into draft sections',
|
|
278
|
+
description: 'Convert paper outline into structured draft sections with claim-evidence alignment.',
|
|
279
|
+
taskType: 'writing',
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: 'publication_finalize_artifacts',
|
|
283
|
+
title: 'Finalize figures, tables, and artifacts',
|
|
284
|
+
description: 'Prepare final visuals and reproducibility artifacts required for submission.',
|
|
285
|
+
taskType: 'writing',
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
recommended_skills: map.publication.base,
|
|
289
|
+
},
|
|
290
|
+
promotion: {
|
|
291
|
+
required_elements: [
|
|
292
|
+
'sections.promotion.slide_outline',
|
|
293
|
+
],
|
|
294
|
+
optional_elements: [
|
|
295
|
+
'sections.promotion.deck_style',
|
|
296
|
+
'sections.promotion.tts_config',
|
|
297
|
+
'sections.promotion.video_assembly_plan',
|
|
298
|
+
'sections.promotion.homepage_plan',
|
|
299
|
+
],
|
|
300
|
+
quality_gate: [
|
|
301
|
+
'Slide outline and homepage plan cover key paper contributions',
|
|
302
|
+
'Deck style defined for visual consistency',
|
|
303
|
+
],
|
|
304
|
+
task_blueprints: [
|
|
305
|
+
{
|
|
306
|
+
id: 'promotion_draft_outline',
|
|
307
|
+
title: 'Draft slide outline and narration scripts',
|
|
308
|
+
description: 'Create per-slide content plan with talking points based on paper contributions.',
|
|
309
|
+
taskType: 'scripting',
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: 'promotion_prepare_homepage',
|
|
313
|
+
title: 'Prepare research homepage content and assets',
|
|
314
|
+
description: 'Organize homepage sections, key visuals, and links for project promotion.',
|
|
315
|
+
taskType: 'delivery',
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: 'promotion_generate_slides',
|
|
319
|
+
title: 'Generate slide images from outline and paper figures',
|
|
320
|
+
description: 'Use nanobanana to render slide images, preferring /edit on existing HQ paper figures.',
|
|
321
|
+
taskType: 'rendering',
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: 'promotion_generate_narration',
|
|
325
|
+
title: 'Generate TTS audio for slide narration',
|
|
326
|
+
description: 'Generate one audio file per slide using edge-tts (default), Kokoro (offline), or ElevenLabs (premium).',
|
|
327
|
+
taskType: 'narration',
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
recommended_skills: map.promotion?.base || map.presentation?.base || ['making-academic-presentations'],
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const TEMPLATES_DIR = path.resolve(__dirname, '..', 'taskmaster-templates');
|
|
336
|
+
const DEFAULT_PIPELINE_CONFIG = {
|
|
337
|
+
version: '1.0',
|
|
338
|
+
provider: 'dr-claw-web',
|
|
339
|
+
initializedAt: new Date().toISOString(),
|
|
340
|
+
};
|
|
341
|
+
let cachedTemplates = null;
|
|
342
|
+
|
|
343
|
+
function getPipelinePaths(projectPath) {
|
|
344
|
+
const pipelineRoot = path.join(projectPath, PIPELINE_DIR);
|
|
345
|
+
return {
|
|
346
|
+
root: pipelineRoot,
|
|
347
|
+
tasksDir: path.join(pipelineRoot, 'tasks'),
|
|
348
|
+
tasksFile: path.join(pipelineRoot, 'tasks', 'tasks.json'),
|
|
349
|
+
docsDir: path.join(pipelineRoot, 'docs'),
|
|
350
|
+
configFile: path.join(pipelineRoot, 'config.json'),
|
|
351
|
+
legacyRoot: path.join(projectPath, LEGACY_TASKMASTER_DIR),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function pathExists(filePath) {
|
|
356
|
+
try {
|
|
357
|
+
await fsPromises.access(filePath);
|
|
358
|
+
return true;
|
|
359
|
+
} catch {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function migrateLegacyTaskmasterIfNeeded(projectPath) {
|
|
365
|
+
const paths = getPipelinePaths(projectPath);
|
|
366
|
+
const hasPipeline = await pathExists(paths.root);
|
|
367
|
+
const hasLegacy = await pathExists(paths.legacyRoot);
|
|
368
|
+
if (hasPipeline || !hasLegacy) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
await fsPromises.cp(paths.legacyRoot, paths.root, { recursive: true, force: false });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function ensurePipelineInitialized(projectPath) {
|
|
376
|
+
const paths = getPipelinePaths(projectPath);
|
|
377
|
+
await migrateLegacyTaskmasterIfNeeded(projectPath);
|
|
378
|
+
await fsPromises.mkdir(paths.tasksDir, { recursive: true });
|
|
379
|
+
await fsPromises.mkdir(paths.docsDir, { recursive: true });
|
|
380
|
+
|
|
381
|
+
if (!(await pathExists(paths.configFile))) {
|
|
382
|
+
await fsPromises.writeFile(paths.configFile, `${JSON.stringify(DEFAULT_PIPELINE_CONFIG, null, 2)}\n`, 'utf8');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (!(await pathExists(paths.tasksFile))) {
|
|
386
|
+
const initial = { [DEFAULT_TASKS_TAG]: { tasks: [] } };
|
|
387
|
+
await fsPromises.writeFile(paths.tasksFile, `${JSON.stringify(initial, null, 2)}\n`, 'utf8');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return paths;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function extractTasksFromData(tasksData) {
|
|
394
|
+
let currentTag = DEFAULT_TASKS_TAG;
|
|
395
|
+
let tasks = [];
|
|
396
|
+
|
|
397
|
+
if (Array.isArray(tasksData)) {
|
|
398
|
+
tasks = tasksData;
|
|
399
|
+
} else if (tasksData?.tasks) {
|
|
400
|
+
tasks = tasksData.tasks;
|
|
401
|
+
} else if (tasksData && typeof tasksData === 'object') {
|
|
402
|
+
if (tasksData[currentTag]?.tasks) {
|
|
403
|
+
tasks = tasksData[currentTag].tasks;
|
|
404
|
+
} else if (tasksData.master?.tasks) {
|
|
405
|
+
tasks = tasksData.master.tasks;
|
|
406
|
+
currentTag = 'master';
|
|
407
|
+
} else {
|
|
408
|
+
const firstTag = Object.keys(tasksData).find((key) => Array.isArray(tasksData[key]?.tasks));
|
|
409
|
+
if (firstTag) {
|
|
410
|
+
currentTag = firstTag;
|
|
411
|
+
tasks = tasksData[firstTag].tasks;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return { tasks: Array.isArray(tasks) ? tasks : [], currentTag };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function normalizeTask(task) {
|
|
420
|
+
const now = new Date().toISOString();
|
|
421
|
+
const stage = normalizeStageName(task.stage);
|
|
422
|
+
return {
|
|
423
|
+
id: task.id,
|
|
424
|
+
title: task.title || 'Untitled Task',
|
|
425
|
+
description: task.description || '',
|
|
426
|
+
status: normalizeTaskStatus(task.status),
|
|
427
|
+
priority: task.priority || 'medium',
|
|
428
|
+
dependencies: Array.isArray(task.dependencies) ? task.dependencies : [],
|
|
429
|
+
createdAt: task.createdAt || task.created || now,
|
|
430
|
+
updatedAt: task.updatedAt || task.updated || now,
|
|
431
|
+
details: task.details || '',
|
|
432
|
+
testStrategy: task.testStrategy || task.test_strategy || '',
|
|
433
|
+
subtasks: Array.isArray(task.subtasks) ? task.subtasks : [],
|
|
434
|
+
stage: stage || undefined,
|
|
435
|
+
taskType: task.taskType || 'implementation',
|
|
436
|
+
inputsNeeded: Array.isArray(task.inputsNeeded) ? task.inputsNeeded.filter(Boolean) : [],
|
|
437
|
+
suggestedSkills: Array.isArray(task.suggestedSkills) ? task.suggestedSkills.filter(Boolean) : [],
|
|
438
|
+
sourceBlueprintId: task.sourceBlueprintId || '',
|
|
439
|
+
nextActionPrompt: typeof task.nextActionPrompt === 'string' ? task.nextActionPrompt : '',
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function normalizeTaskStatus(status) {
|
|
444
|
+
const raw = String(status || '').trim().toLowerCase();
|
|
445
|
+
if (!raw) return 'pending';
|
|
446
|
+
if (raw === 'completed' || raw === 'complete') return 'done';
|
|
447
|
+
if (raw === 'in_progress' || raw === 'inprogress') return 'in-progress';
|
|
448
|
+
if (raw === 'todo' || raw === 'open') return 'pending';
|
|
449
|
+
return raw;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function readTasksFile(tasksFilePath) {
|
|
453
|
+
const content = await fsPromises.readFile(tasksFilePath, 'utf8');
|
|
454
|
+
const parsed = JSON.parse(content);
|
|
455
|
+
const { tasks, currentTag } = extractTasksFromData(parsed);
|
|
456
|
+
return {
|
|
457
|
+
raw: parsed,
|
|
458
|
+
currentTag,
|
|
459
|
+
tasks: tasks.map(normalizeTask),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function writeTasksFile(tasksFilePath, tasks, currentTag = DEFAULT_TASKS_TAG) {
|
|
464
|
+
const payload = { [currentTag]: { tasks } };
|
|
465
|
+
await fsPromises.writeFile(tasksFilePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function generateTaskId(tasks) {
|
|
469
|
+
const numericIds = tasks
|
|
470
|
+
.map((task) => Number(task.id))
|
|
471
|
+
.filter((value) => Number.isFinite(value));
|
|
472
|
+
if (numericIds.length === 0) {
|
|
473
|
+
return 1;
|
|
474
|
+
}
|
|
475
|
+
return Math.max(...numericIds) + 1;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Reassign all task IDs sequentially (1, 2, 3, ...) following global stage order.
|
|
480
|
+
* Preserves array order within each stage group and remaps dependency references.
|
|
481
|
+
*/
|
|
482
|
+
function reindexTasks(tasks) {
|
|
483
|
+
const staged = {};
|
|
484
|
+
STAGE_ORDER.forEach((s) => { staged[s] = []; });
|
|
485
|
+
const unassigned = [];
|
|
486
|
+
|
|
487
|
+
for (const task of tasks) {
|
|
488
|
+
const s = normalizeStageName(task.stage);
|
|
489
|
+
if (s && staged[s]) {
|
|
490
|
+
staged[s].push(task);
|
|
491
|
+
} else {
|
|
492
|
+
unassigned.push(task);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const ordered = [];
|
|
497
|
+
STAGE_ORDER.forEach((s) => { ordered.push(...staged[s]); });
|
|
498
|
+
ordered.push(...unassigned);
|
|
499
|
+
|
|
500
|
+
const idMap = {};
|
|
501
|
+
ordered.forEach((task, idx) => {
|
|
502
|
+
idMap[String(task.id)] = idx + 1;
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
return ordered.map((task, idx) => ({
|
|
506
|
+
...task,
|
|
507
|
+
id: idx + 1,
|
|
508
|
+
dependencies: Array.isArray(task.dependencies)
|
|
509
|
+
? task.dependencies
|
|
510
|
+
.map((dep) => idMap[String(dep)])
|
|
511
|
+
.filter(Boolean)
|
|
512
|
+
: [],
|
|
513
|
+
updatedAt: new Date().toISOString(),
|
|
514
|
+
}));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function splitPromptToTitle(prompt) {
|
|
518
|
+
const cleaned = String(prompt || '').replace(/\s+/g, ' ').trim();
|
|
519
|
+
if (!cleaned) {
|
|
520
|
+
return 'Untitled Task';
|
|
521
|
+
}
|
|
522
|
+
return cleaned.length > 120 ? `${cleaned.slice(0, 117)}...` : cleaned;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function assignPath(target, dottedPath, value) {
|
|
526
|
+
const keys = String(dottedPath || '').split('.').filter(Boolean);
|
|
527
|
+
if (keys.length === 0) return;
|
|
528
|
+
let cursor = target;
|
|
529
|
+
for (let i = 0; i < keys.length - 1; i += 1) {
|
|
530
|
+
const key = keys[i];
|
|
531
|
+
if (!cursor[key] || typeof cursor[key] !== 'object' || Array.isArray(cursor[key])) {
|
|
532
|
+
cursor[key] = {};
|
|
533
|
+
}
|
|
534
|
+
cursor = cursor[key];
|
|
535
|
+
}
|
|
536
|
+
cursor[keys[keys.length - 1]] = value;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function toTaskCandidate(raw) {
|
|
540
|
+
const value = String(raw || '').replace(/\s+/g, ' ').trim();
|
|
541
|
+
if (!value) return null;
|
|
542
|
+
return value.length > 180 ? `${value.slice(0, 177)}...` : value;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function isPlaceholderLikeValue(value = '') {
|
|
546
|
+
const normalized = String(value).trim().toLowerCase();
|
|
547
|
+
if (!normalized) return true;
|
|
548
|
+
const blocked = new Set([
|
|
549
|
+
'none', 'null', 'n/a', 'na', 'todo', 'tbd', 'unknown', 'not sure', '-',
|
|
550
|
+
'[]', '{}', 'placeholder',
|
|
551
|
+
]);
|
|
552
|
+
return blocked.has(normalized);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function buildFallbackTaskCandidates(briefData = {}) {
|
|
556
|
+
const title = String(briefData?.meta?.title || '').trim();
|
|
557
|
+
const target = title ? ` for "${title}"` : '';
|
|
558
|
+
return [
|
|
559
|
+
`Finalize Survey section${target}: define literature scope, summarize prior work, and record open gaps.`,
|
|
560
|
+
`Finalize Ideation section${target}: clarify research goal, framing, and evidence plan.`,
|
|
561
|
+
`Define Experiment section${target}: specify validation goal, protocol, and evaluation plan.`,
|
|
562
|
+
`Prepare Publication section${target}: draft paper outline, figures/tables plan, and submission checklist.`,
|
|
563
|
+
`Create Promotion section${target}: draft homepage plan, slide outline, deck style, TTS config, and video assembly plan.`,
|
|
564
|
+
];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function buildPipelineSkeletonCandidates(briefData = {}) {
|
|
568
|
+
const title = String(briefData?.meta?.title || '').trim();
|
|
569
|
+
const target = title ? ` (${title})` : '';
|
|
570
|
+
return [
|
|
571
|
+
`Survey: define literature scope and search boundary${target}.`,
|
|
572
|
+
`Survey: synthesize key baselines, trends, and open gaps${target}.`,
|
|
573
|
+
`Ideation: clarify problem framing and research goal${target}.`,
|
|
574
|
+
`Ideation: collect key evidence and references${target}.`,
|
|
575
|
+
`Ideation: define measurable success criteria${target}.`,
|
|
576
|
+
`Experiment: define validation hypothesis and evaluation criteria${target}.`,
|
|
577
|
+
`Experiment: prepare data source and method/protocol plan${target}.`,
|
|
578
|
+
`Experiment: execute baseline validation and analyze results${target}.`,
|
|
579
|
+
`Publication: draft paper outline and contribution boundaries${target}.`,
|
|
580
|
+
`Publication: prepare figures/tables and artifact appendix${target}.`,
|
|
581
|
+
`Publication: complete submission checklist and final review${target}.`,
|
|
582
|
+
`Promotion: prepare project homepage structure and assets${target}.`,
|
|
583
|
+
`Promotion: draft slide outline and narration scripts${target}.`,
|
|
584
|
+
`Promotion: generate slide images from paper figures${target}.`,
|
|
585
|
+
`Promotion: generate TTS audio and assemble demo video${target}.`,
|
|
586
|
+
];
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function parseBriefJsonToTaskCandidates(briefData = {}) {
|
|
590
|
+
const candidates = [];
|
|
591
|
+
const sectionOrder = ['ideation', 'experiment', 'publication', 'promotion', 'presentation'];
|
|
592
|
+
const sectionData = briefData?.sections && typeof briefData.sections === 'object'
|
|
593
|
+
? briefData.sections
|
|
594
|
+
: {};
|
|
595
|
+
|
|
596
|
+
sectionOrder.forEach((sectionName) => {
|
|
597
|
+
const fields = sectionData[sectionName];
|
|
598
|
+
if (!fields || typeof fields !== 'object') return;
|
|
599
|
+
Object.values(fields).forEach((value) => {
|
|
600
|
+
if (Array.isArray(value)) {
|
|
601
|
+
value.forEach((item) => {
|
|
602
|
+
const normalized = toTaskCandidate(item);
|
|
603
|
+
if (normalized) candidates.push(normalized);
|
|
604
|
+
});
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const normalized = toTaskCandidate(value);
|
|
608
|
+
if (normalized && !isPlaceholderLikeValue(normalized)) candidates.push(normalized);
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const dynamic = [...new Set(candidates)];
|
|
613
|
+
const skeleton = buildPipelineSkeletonCandidates(briefData);
|
|
614
|
+
const fallback = dynamic.length > 0 ? [] : buildFallbackTaskCandidates(briefData);
|
|
615
|
+
return [...new Set([...skeleton, ...dynamic, ...fallback])];
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function inferStageFromCandidate(text = '') {
|
|
619
|
+
const value = String(text || '').toLowerCase();
|
|
620
|
+
if (value.includes('ideation')) return 'ideation';
|
|
621
|
+
if (value.includes('experiment') || value.includes('validation') || value.includes('baseline')) return 'experiment';
|
|
622
|
+
if (value.includes('publication') || value.includes('paper') || value.includes('submission')) return 'publication';
|
|
623
|
+
if (
|
|
624
|
+
value.includes('promotion')
|
|
625
|
+
|| value.includes('presentation')
|
|
626
|
+
|| value.includes('slide')
|
|
627
|
+
|| value.includes('deck')
|
|
628
|
+
|| value.includes('demo video')
|
|
629
|
+
|| value.includes('homepage')
|
|
630
|
+
) return 'promotion';
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function normalizeStageName(stage) {
|
|
635
|
+
const value = String(stage || '').trim().toLowerCase();
|
|
636
|
+
if (value === 'presentation') return 'promotion';
|
|
637
|
+
if (value === 'research') return 'survey';
|
|
638
|
+
if (value === 'survey' || value === 'ideation' || value === 'experiment' || value === 'publication' || value === 'promotion') {
|
|
639
|
+
return value;
|
|
640
|
+
}
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function titleFromBlueprintId(sourceBlueprintId = '', stage = '') {
|
|
645
|
+
const cleaned = String(sourceBlueprintId || '').replace(/[_-]+/g, ' ').trim();
|
|
646
|
+
const title = cleaned
|
|
647
|
+
? cleaned.replace(/\b\w/g, (ch) => ch.toUpperCase())
|
|
648
|
+
: `Execute ${STAGE_LABELS[stage] || 'Pipeline'} task`;
|
|
649
|
+
return title.length > 120 ? `${title.slice(0, 117)}...` : title;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function getValueByPath(target, dottedPath) {
|
|
653
|
+
if (!target || typeof target !== 'object') return undefined;
|
|
654
|
+
const keys = String(dottedPath || '').split('.').filter(Boolean);
|
|
655
|
+
if (keys.length === 0) return undefined;
|
|
656
|
+
let cursor = target;
|
|
657
|
+
for (const key of keys) {
|
|
658
|
+
if (cursor === null || cursor === undefined || typeof cursor !== 'object') {
|
|
659
|
+
return undefined;
|
|
660
|
+
}
|
|
661
|
+
cursor = cursor[key];
|
|
662
|
+
}
|
|
663
|
+
return cursor;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function hasMeaningfulValue(value) {
|
|
667
|
+
if (value === null || value === undefined) return false;
|
|
668
|
+
if (typeof value === 'string') return !isPlaceholderLikeValue(value);
|
|
669
|
+
if (Array.isArray(value)) return value.some((item) => hasMeaningfulValue(item));
|
|
670
|
+
if (typeof value === 'object') return Object.keys(value).length > 0;
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function computeMissingElements(briefData = {}, requiredElements = []) {
|
|
675
|
+
if (!Array.isArray(requiredElements)) return [];
|
|
676
|
+
return requiredElements
|
|
677
|
+
.map((pathName) => String(pathName || '').trim())
|
|
678
|
+
.filter(Boolean)
|
|
679
|
+
.filter((pathName) => !hasMeaningfulValue(getValueByPath(briefData, pathName)));
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function ensureArray(value) {
|
|
683
|
+
if (!value) return [];
|
|
684
|
+
if (Array.isArray(value)) return value;
|
|
685
|
+
return [value];
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function dedupeStringList(values = []) {
|
|
689
|
+
return [...new Set(values.map((item) => String(item || '').trim()).filter(Boolean))];
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function formatInputValue(value) {
|
|
693
|
+
if (value === null || value === undefined) return '';
|
|
694
|
+
if (typeof value === 'string') {
|
|
695
|
+
return value.trim();
|
|
696
|
+
}
|
|
697
|
+
if (Array.isArray(value)) {
|
|
698
|
+
return value
|
|
699
|
+
.map((item) => String(item ?? '').trim())
|
|
700
|
+
.filter(Boolean)
|
|
701
|
+
.join('\n');
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
return JSON.stringify(value, null, 2);
|
|
705
|
+
} catch {
|
|
706
|
+
return String(value);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function toTitleCase(text = '') {
|
|
711
|
+
return String(text || '')
|
|
712
|
+
.split(/\s+/)
|
|
713
|
+
.filter(Boolean)
|
|
714
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
715
|
+
.join(' ');
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function formatFieldDisplayName(pathName = '') {
|
|
719
|
+
const cleaned = String(pathName || '').trim();
|
|
720
|
+
if (!cleaned) return 'Required Field';
|
|
721
|
+
const parts = cleaned.split('.').filter(Boolean);
|
|
722
|
+
const fieldKey = parts[parts.length - 1] || cleaned;
|
|
723
|
+
const stageKey = parts[1] && STAGE_LABELS[parts[1]] ? STAGE_LABELS[parts[1]] : '';
|
|
724
|
+
const fieldLabel = toTitleCase(fieldKey.replace(/[_-]+/g, ' '));
|
|
725
|
+
return stageKey ? `${stageKey} ${fieldLabel}` : fieldLabel;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function resolveTaskSkills(stage, taskType, stageConfiguredSkills = [], blueprintSkills = []) {
|
|
729
|
+
const stageMap = getStageSkillMap()[stage] || {};
|
|
730
|
+
const fromStageBase = ensureArray(stageMap.base);
|
|
731
|
+
const fromTaskType = ensureArray(stageMap.byTaskType?.[taskType]);
|
|
732
|
+
return dedupeStringList([
|
|
733
|
+
...ensureArray(stageConfiguredSkills),
|
|
734
|
+
...fromStageBase,
|
|
735
|
+
...fromTaskType,
|
|
736
|
+
...ensureArray(blueprintSkills),
|
|
737
|
+
]);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function buildTaskNextActionPrompt(task = {}, stageConfig = {}, stage = '', briefData = {}) {
|
|
741
|
+
const lines = [
|
|
742
|
+
`Task: ${task.title || 'Untitled Task'}`,
|
|
743
|
+
`Stage: ${STAGE_LABELS[stage] || stage || 'Unknown'}`,
|
|
744
|
+
];
|
|
745
|
+
|
|
746
|
+
const requiredInputs = dedupeStringList(Array.isArray(task.inputsNeeded) ? task.inputsNeeded : []);
|
|
747
|
+
const missingInputs = [];
|
|
748
|
+
const providedInputs = [];
|
|
749
|
+
requiredInputs.forEach((pathName) => {
|
|
750
|
+
const value = getValueByPath(briefData, pathName);
|
|
751
|
+
if (hasMeaningfulValue(value)) {
|
|
752
|
+
providedInputs.push({ pathName, value: formatInputValue(value) });
|
|
753
|
+
} else {
|
|
754
|
+
missingInputs.push(pathName);
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
if (missingInputs.length > 0) {
|
|
759
|
+
lines.push(`Missing inputs: ${missingInputs.join(', ')}`);
|
|
760
|
+
}
|
|
761
|
+
if (providedInputs.length === 1) {
|
|
762
|
+
lines.push(`User inputs: "${providedInputs[0].value}"`);
|
|
763
|
+
} else if (providedInputs.length > 1) {
|
|
764
|
+
lines.push('User inputs:');
|
|
765
|
+
providedInputs.forEach((entry) => {
|
|
766
|
+
lines.push(`- ${entry.pathName}: "${entry.value}"`);
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const suggestedSkills = Array.isArray(task.suggestedSkills) ? task.suggestedSkills : [];
|
|
771
|
+
if (suggestedSkills.length > 0) {
|
|
772
|
+
lines.push(`Suggested skills: ${suggestedSkills.join(', ')}`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const qualityGate = Array.isArray(stageConfig.quality_gate) ? stageConfig.quality_gate : [];
|
|
776
|
+
if (qualityGate.length > 0 && task.taskType === 'analysis') {
|
|
777
|
+
lines.push(`Quality gate checklist: ${qualityGate.join(' | ')}`);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (STAGE_PROMPT_HINTS[stage]) {
|
|
781
|
+
lines.push(`Stage guidance: ${STAGE_PROMPT_HINTS[stage]}`);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (providedInputs.length > 0) {
|
|
785
|
+
lines.push('Please produce a concrete next step plan and execution output. If user inputs are provided, polish and make them concrete, then write updates back to .pipeline/docs/research_brief.json.');
|
|
786
|
+
} else {
|
|
787
|
+
lines.push('Please produce a concrete next step plan and execution output. If key inputs are missing, propose precise placeholders and write updates back to .pipeline/docs/research_brief.json.');
|
|
788
|
+
}
|
|
789
|
+
return lines.join('\n');
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function instantiatePipelineTasksFromBrief(briefData = {}, numTasks = DEFAULT_MAX_TASKS) {
|
|
793
|
+
const pipelineStages = briefData?.pipeline?.stages && typeof briefData.pipeline.stages === 'object'
|
|
794
|
+
? briefData.pipeline.stages
|
|
795
|
+
: null;
|
|
796
|
+
if (!pipelineStages) return null;
|
|
797
|
+
|
|
798
|
+
const now = new Date().toISOString();
|
|
799
|
+
const generated = [];
|
|
800
|
+
const maxTasks = Number.isFinite(Number(numTasks)) && Number(numTasks) > 0 ? Number(numTasks) : DEFAULT_MAX_TASKS;
|
|
801
|
+
|
|
802
|
+
// Respect pipeline.startStage — only generate tasks for stages >= startStage
|
|
803
|
+
const startStage = normalizeStageName(briefData?.pipeline?.startStage) || 'survey';
|
|
804
|
+
const startIdx = STAGE_ORDER.indexOf(startStage);
|
|
805
|
+
const activeStages = startIdx > 0 ? STAGE_ORDER.slice(startIdx) : STAGE_ORDER;
|
|
806
|
+
|
|
807
|
+
for (const stage of activeStages) {
|
|
808
|
+
const stageConfig = pipelineStages?.[stage]
|
|
809
|
+
|| (stage === 'promotion' ? pipelineStages?.presentation : null);
|
|
810
|
+
if (!stageConfig || typeof stageConfig !== 'object') continue;
|
|
811
|
+
|
|
812
|
+
const stageSkills = resolveTaskSkills(stage, '', stageConfig.recommended_skills, []);
|
|
813
|
+
const stageRequiredElements = dedupeStringList(ensureArray(stageConfig.required_elements));
|
|
814
|
+
const missingElements = computeMissingElements(briefData, stageConfig.required_elements);
|
|
815
|
+
const stageBlueprints = ensureArray(stageConfig.task_blueprints);
|
|
816
|
+
|
|
817
|
+
stageBlueprints.forEach((blueprintNode, index) => {
|
|
818
|
+
const blueprint = typeof blueprintNode === 'string'
|
|
819
|
+
? { id: blueprintNode }
|
|
820
|
+
: (blueprintNode && typeof blueprintNode === 'object' ? blueprintNode : { id: `${stage}_task_${index + 1}` });
|
|
821
|
+
const sourceBlueprintId = String(blueprint.id || `${stage}_task_${index + 1}`);
|
|
822
|
+
const task = normalizeTask({
|
|
823
|
+
id: generated.length + 1,
|
|
824
|
+
title: blueprint.title || titleFromBlueprintId(sourceBlueprintId, stage),
|
|
825
|
+
description: blueprint.description || `Execute ${STAGE_LABELS[stage] || stage} task from pipeline blueprint.`,
|
|
826
|
+
status: 'pending',
|
|
827
|
+
priority: blueprint.priority || 'medium',
|
|
828
|
+
dependencies: Array.isArray(blueprint.dependencies) ? blueprint.dependencies : [],
|
|
829
|
+
createdAt: now,
|
|
830
|
+
updatedAt: now,
|
|
831
|
+
stage,
|
|
832
|
+
taskType: blueprint.taskType || 'implementation',
|
|
833
|
+
inputsNeeded: dedupeStringList([
|
|
834
|
+
...ensureArray(blueprint.inputsNeeded),
|
|
835
|
+
...stageRequiredElements,
|
|
836
|
+
]),
|
|
837
|
+
suggestedSkills: resolveTaskSkills(stage, blueprint.taskType || 'implementation', stageSkills, blueprint.recommended_skills),
|
|
838
|
+
sourceBlueprintId,
|
|
839
|
+
nextActionPrompt: blueprint.nextActionPrompt || '',
|
|
840
|
+
});
|
|
841
|
+
task.nextActionPrompt = task.nextActionPrompt || buildTaskNextActionPrompt(task, stageConfig, stage, briefData);
|
|
842
|
+
generated.push(task);
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
stageRequiredElements.forEach((requiredPath) => {
|
|
846
|
+
const value = getValueByPath(briefData, requiredPath);
|
|
847
|
+
const hasValue = hasMeaningfulValue(value);
|
|
848
|
+
const fieldDisplayName = formatFieldDisplayName(requiredPath);
|
|
849
|
+
const task = normalizeTask({
|
|
850
|
+
id: generated.length + 1,
|
|
851
|
+
title: hasValue ? `Refine ${fieldDisplayName}` : `Define ${fieldDisplayName}`,
|
|
852
|
+
description: hasValue
|
|
853
|
+
? `The required field "${requiredPath}" exists but may still be vague. Refine it into concrete, testable language.`
|
|
854
|
+
: `The required field "${requiredPath}" is missing or unclear. Clarify it before stage completion.`,
|
|
855
|
+
status: 'pending',
|
|
856
|
+
priority: hasValue ? 'medium' : 'high',
|
|
857
|
+
dependencies: [],
|
|
858
|
+
createdAt: now,
|
|
859
|
+
updatedAt: now,
|
|
860
|
+
stage,
|
|
861
|
+
taskType: hasValue ? 'analysis' : 'exploration',
|
|
862
|
+
inputsNeeded: [requiredPath],
|
|
863
|
+
suggestedSkills: resolveTaskSkills(stage, hasValue ? 'analysis' : 'exploration', stageSkills, []),
|
|
864
|
+
sourceBlueprintId: hasValue ? `${stage}.refine.${requiredPath}` : `${stage}.missing.${requiredPath}`,
|
|
865
|
+
nextActionPrompt: '',
|
|
866
|
+
});
|
|
867
|
+
task.nextActionPrompt = buildTaskNextActionPrompt(task, stageConfig, stage, briefData);
|
|
868
|
+
generated.push(task);
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
const qualityGate = Array.isArray(stageConfig.quality_gate)
|
|
872
|
+
? stageConfig.quality_gate.map((item) => String(item || '').trim()).filter(Boolean)
|
|
873
|
+
: [];
|
|
874
|
+
if (qualityGate.length > 0) {
|
|
875
|
+
const task = normalizeTask({
|
|
876
|
+
id: generated.length + 1,
|
|
877
|
+
title: `Review ${STAGE_LABELS[stage] || stage} quality gate before moving forward`,
|
|
878
|
+
description: `Complete and verify ${STAGE_LABELS[stage] || stage} quality gate criteria.`,
|
|
879
|
+
status: 'pending',
|
|
880
|
+
priority: 'medium',
|
|
881
|
+
dependencies: [],
|
|
882
|
+
createdAt: now,
|
|
883
|
+
updatedAt: now,
|
|
884
|
+
stage,
|
|
885
|
+
taskType: 'analysis',
|
|
886
|
+
inputsNeeded: [],
|
|
887
|
+
suggestedSkills: resolveTaskSkills(stage, 'analysis', stageSkills, []),
|
|
888
|
+
sourceBlueprintId: `${stage}.quality_gate`,
|
|
889
|
+
nextActionPrompt: '',
|
|
890
|
+
});
|
|
891
|
+
task.nextActionPrompt = buildTaskNextActionPrompt(task, stageConfig, stage, briefData);
|
|
892
|
+
generated.push(task);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const normalized = generated.slice(0, maxTasks).map((task, index) => normalizeTask({
|
|
897
|
+
...task,
|
|
898
|
+
id: index + 1,
|
|
899
|
+
}));
|
|
900
|
+
|
|
901
|
+
return normalized.length > 0 ? normalized : null;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function computeNextGuidance(tasks = []) {
|
|
905
|
+
const allTasks = Array.isArray(tasks) ? tasks : [];
|
|
906
|
+
const doneIds = new Set(
|
|
907
|
+
allTasks
|
|
908
|
+
.filter((task) => String(task.status || '').toLowerCase() === 'done')
|
|
909
|
+
.map((task) => String(task.id)),
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
const inProgress = allTasks.find((task) => String(task.status || '').toLowerCase() === 'in-progress') || null;
|
|
913
|
+
if (inProgress) {
|
|
914
|
+
return {
|
|
915
|
+
nextTask: inProgress,
|
|
916
|
+
whyNext: 'This task is already in progress and should be finished first.',
|
|
917
|
+
requiredInputs: Array.isArray(inProgress.inputsNeeded) ? inProgress.inputsNeeded : [],
|
|
918
|
+
suggestedSkills: Array.isArray(inProgress.suggestedSkills) ? inProgress.suggestedSkills : [],
|
|
919
|
+
nextActionPrompt: inProgress.nextActionPrompt || '',
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const pendingTasks = allTasks.filter((task) => String(task.status || '').toLowerCase() === 'pending');
|
|
924
|
+
if (pendingTasks.length === 0) {
|
|
925
|
+
return {
|
|
926
|
+
nextTask: null,
|
|
927
|
+
whyNext: 'No pending or in-progress tasks available.',
|
|
928
|
+
requiredInputs: [],
|
|
929
|
+
suggestedSkills: [],
|
|
930
|
+
nextActionPrompt: '',
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const readyTask = pendingTasks.find((task) => {
|
|
935
|
+
const deps = Array.isArray(task.dependencies) ? task.dependencies.map((dep) => String(dep)) : [];
|
|
936
|
+
return deps.every((dep) => doneIds.has(dep));
|
|
937
|
+
}) || pendingTasks[0];
|
|
938
|
+
|
|
939
|
+
const blockedDependencies = Array.isArray(readyTask.dependencies)
|
|
940
|
+
? readyTask.dependencies.filter((dep) => !doneIds.has(String(dep)))
|
|
941
|
+
: [];
|
|
942
|
+
const whyNext = blockedDependencies.length === 0
|
|
943
|
+
? 'This is the first actionable pending task based on dependency order.'
|
|
944
|
+
: `This pending task is recommended, but it still references unresolved dependencies: ${blockedDependencies.join(', ')}`;
|
|
945
|
+
|
|
946
|
+
return {
|
|
947
|
+
nextTask: readyTask,
|
|
948
|
+
whyNext,
|
|
949
|
+
requiredInputs: Array.isArray(readyTask.inputsNeeded) ? readyTask.inputsNeeded : [],
|
|
950
|
+
suggestedSkills: Array.isArray(readyTask.suggestedSkills) ? readyTask.suggestedSkills : [],
|
|
951
|
+
nextActionPrompt: readyTask.nextActionPrompt || '',
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function computeTaskmasterStatus(taskMasterResult, mcpResult) {
|
|
956
|
+
let status = 'not-configured';
|
|
957
|
+
if (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles) {
|
|
958
|
+
if (mcpResult.hasMCPServer && mcpResult.isConfigured) {
|
|
959
|
+
status = 'fully-configured';
|
|
960
|
+
} else {
|
|
961
|
+
status = 'taskmaster-only';
|
|
962
|
+
}
|
|
963
|
+
} else if (mcpResult.hasMCPServer && mcpResult.isConfigured) {
|
|
964
|
+
status = 'mcp-only';
|
|
965
|
+
}
|
|
966
|
+
return status;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function buildTaskmasterSummaryPayload({
|
|
970
|
+
projectName,
|
|
971
|
+
projectPath,
|
|
972
|
+
status,
|
|
973
|
+
tasks = [],
|
|
974
|
+
nextTask = null,
|
|
975
|
+
guidance = null,
|
|
976
|
+
updatedAt = new Date().toISOString(),
|
|
977
|
+
}) {
|
|
978
|
+
const tasksByStatus = tasks.reduce((acc, task) => {
|
|
979
|
+
const taskStatus = normalizeTaskStatus(task.status);
|
|
980
|
+
acc[taskStatus] = (acc[taskStatus] || 0) + 1;
|
|
981
|
+
return acc;
|
|
982
|
+
}, {
|
|
983
|
+
pending: 0,
|
|
984
|
+
'in-progress': 0,
|
|
985
|
+
done: 0,
|
|
986
|
+
review: 0,
|
|
987
|
+
deferred: 0,
|
|
988
|
+
cancelled: 0,
|
|
989
|
+
blocked: 0,
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
const total = tasks.length;
|
|
993
|
+
const completed = tasksByStatus.done || 0;
|
|
994
|
+
|
|
995
|
+
return {
|
|
996
|
+
project: projectName,
|
|
997
|
+
status,
|
|
998
|
+
project_path: projectPath,
|
|
999
|
+
counts: {
|
|
1000
|
+
total,
|
|
1001
|
+
completed,
|
|
1002
|
+
in_progress: tasksByStatus['in-progress'] || 0,
|
|
1003
|
+
pending: tasksByStatus.pending || 0,
|
|
1004
|
+
blocked: tasksByStatus.blocked || 0,
|
|
1005
|
+
review: tasksByStatus.review || 0,
|
|
1006
|
+
deferred: tasksByStatus.deferred || 0,
|
|
1007
|
+
cancelled: tasksByStatus.cancelled || 0,
|
|
1008
|
+
completion_rate: total > 0 ? Math.round((completed / total) * 1000) / 10 : 0,
|
|
1009
|
+
},
|
|
1010
|
+
next_task: nextTask,
|
|
1011
|
+
guidance,
|
|
1012
|
+
updated_at: updatedAt,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function dedupeGeneratedTasks(existingTasks = [], generatedTasks = []) {
|
|
1017
|
+
const signature = (task) => `${String(task.title || '').trim().toLowerCase()}|${String(task.description || '').trim().toLowerCase()}`;
|
|
1018
|
+
const existingSignatures = new Set(existingTasks.map(signature));
|
|
1019
|
+
return generatedTasks.filter((task) => {
|
|
1020
|
+
const key = signature(task);
|
|
1021
|
+
if (existingSignatures.has(key)) return false;
|
|
1022
|
+
existingSignatures.add(key);
|
|
1023
|
+
return true;
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Check if TaskMaster CLI is installed globally
|
|
1029
|
+
* @returns {Promise<Object>} Installation status result
|
|
1030
|
+
*/
|
|
1031
|
+
async function checkTaskMasterInstallation() {
|
|
1032
|
+
return {
|
|
1033
|
+
isInstalled: true,
|
|
1034
|
+
installPath: PIPELINE_DIR,
|
|
1035
|
+
version: 'web-native',
|
|
1036
|
+
reason: null,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Detect .pipeline folder presence in a given project directory
|
|
1042
|
+
* @param {string} projectPath - Absolute path to project directory
|
|
1043
|
+
* @returns {Promise<Object>} Detection result with status and metadata
|
|
1044
|
+
*/
|
|
1045
|
+
async function detectTaskMasterFolder(projectPath) {
|
|
1046
|
+
try {
|
|
1047
|
+
await migrateLegacyTaskmasterIfNeeded(projectPath);
|
|
1048
|
+
const taskMasterPath = getPipelinePaths(projectPath).root;
|
|
1049
|
+
|
|
1050
|
+
// Check if .pipeline directory exists
|
|
1051
|
+
try {
|
|
1052
|
+
const stats = await fsPromises.stat(taskMasterPath);
|
|
1053
|
+
if (!stats.isDirectory()) {
|
|
1054
|
+
return {
|
|
1055
|
+
hasTaskmaster: false,
|
|
1056
|
+
reason: '.pipeline exists but is not a directory'
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
if (error.code === 'ENOENT') {
|
|
1061
|
+
return {
|
|
1062
|
+
hasTaskmaster: false,
|
|
1063
|
+
reason: '.pipeline directory not found'
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
throw error;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Check for key TaskMaster files
|
|
1070
|
+
const keyFiles = [
|
|
1071
|
+
'tasks/tasks.json',
|
|
1072
|
+
'docs/research_brief.json',
|
|
1073
|
+
'config.json'
|
|
1074
|
+
];
|
|
1075
|
+
|
|
1076
|
+
const fileStatus = {};
|
|
1077
|
+
let hasEssentialFiles = true;
|
|
1078
|
+
|
|
1079
|
+
for (const file of keyFiles) {
|
|
1080
|
+
const filePath = path.join(taskMasterPath, file);
|
|
1081
|
+
try {
|
|
1082
|
+
await fsPromises.access(filePath, fs.constants.R_OK);
|
|
1083
|
+
fileStatus[file] = true;
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
fileStatus[file] = false;
|
|
1086
|
+
if (file === 'tasks/tasks.json') {
|
|
1087
|
+
hasEssentialFiles = false;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Parse tasks.json if it exists for metadata
|
|
1093
|
+
let taskMetadata = null;
|
|
1094
|
+
if (fileStatus['tasks/tasks.json']) {
|
|
1095
|
+
try {
|
|
1096
|
+
const tasksPath = path.join(taskMasterPath, 'tasks/tasks.json');
|
|
1097
|
+
const tasksContent = await fsPromises.readFile(tasksPath, 'utf8');
|
|
1098
|
+
const tasksData = JSON.parse(tasksContent);
|
|
1099
|
+
const { tasks } = extractTasksFromData(tasksData);
|
|
1100
|
+
|
|
1101
|
+
// Calculate task statistics
|
|
1102
|
+
const stats = tasks.reduce((acc, task) => {
|
|
1103
|
+
const taskStatus = normalizeTaskStatus(task.status);
|
|
1104
|
+
acc.total++;
|
|
1105
|
+
acc[taskStatus] = (acc[taskStatus] || 0) + 1;
|
|
1106
|
+
|
|
1107
|
+
// Count subtasks
|
|
1108
|
+
if (task.subtasks) {
|
|
1109
|
+
task.subtasks.forEach(subtask => {
|
|
1110
|
+
const subtaskStatus = normalizeTaskStatus(subtask.status);
|
|
1111
|
+
acc.subtotalTasks++;
|
|
1112
|
+
acc.subtasks = acc.subtasks || {};
|
|
1113
|
+
acc.subtasks[subtaskStatus] = (acc.subtasks[subtaskStatus] || 0) + 1;
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
return acc;
|
|
1118
|
+
}, {
|
|
1119
|
+
total: 0,
|
|
1120
|
+
subtotalTasks: 0,
|
|
1121
|
+
pending: 0,
|
|
1122
|
+
'in-progress': 0,
|
|
1123
|
+
done: 0,
|
|
1124
|
+
review: 0,
|
|
1125
|
+
deferred: 0,
|
|
1126
|
+
cancelled: 0,
|
|
1127
|
+
subtasks: {}
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
taskMetadata = {
|
|
1131
|
+
taskCount: stats.total,
|
|
1132
|
+
subtaskCount: stats.subtotalTasks,
|
|
1133
|
+
completed: stats.done || 0,
|
|
1134
|
+
pending: stats.pending || 0,
|
|
1135
|
+
inProgress: stats['in-progress'] || 0,
|
|
1136
|
+
review: stats.review || 0,
|
|
1137
|
+
completionPercentage: stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0,
|
|
1138
|
+
lastModified: (await fsPromises.stat(tasksPath)).mtime.toISOString()
|
|
1139
|
+
};
|
|
1140
|
+
} catch (parseError) {
|
|
1141
|
+
console.warn('Failed to parse tasks.json:', parseError.message);
|
|
1142
|
+
taskMetadata = { error: 'Failed to parse tasks.json' };
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const hasResearchBrief = fileStatus['docs/research_brief.json'] === true;
|
|
1147
|
+
|
|
1148
|
+
return {
|
|
1149
|
+
hasTaskmaster: true,
|
|
1150
|
+
hasEssentialFiles,
|
|
1151
|
+
hasResearchBrief,
|
|
1152
|
+
files: fileStatus,
|
|
1153
|
+
metadata: taskMetadata,
|
|
1154
|
+
path: taskMasterPath
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
console.error('Error detecting TaskMaster folder:', error);
|
|
1159
|
+
return {
|
|
1160
|
+
hasTaskmaster: false,
|
|
1161
|
+
reason: `Error checking directory: ${error.message}`
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// MCP detection is now handled by the centralized utility
|
|
1167
|
+
|
|
1168
|
+
// API Routes
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* GET /api/taskmaster/installation-status
|
|
1172
|
+
* Check if TaskMaster CLI is installed on the system
|
|
1173
|
+
*/
|
|
1174
|
+
router.get('/installation-status', async (req, res) => {
|
|
1175
|
+
try {
|
|
1176
|
+
const installationStatus = await checkTaskMasterInstallation();
|
|
1177
|
+
|
|
1178
|
+
// Also check for MCP server configuration
|
|
1179
|
+
const mcpStatus = await detectTaskMasterMCPServer();
|
|
1180
|
+
|
|
1181
|
+
res.json({
|
|
1182
|
+
success: true,
|
|
1183
|
+
installation: installationStatus,
|
|
1184
|
+
mcpServer: mcpStatus,
|
|
1185
|
+
isReady: installationStatus.isInstalled && mcpStatus.hasMCPServer
|
|
1186
|
+
});
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
console.error('Error checking TaskMaster installation:', error);
|
|
1189
|
+
res.status(500).json({
|
|
1190
|
+
success: false,
|
|
1191
|
+
error: 'Failed to check TaskMaster installation status',
|
|
1192
|
+
installation: {
|
|
1193
|
+
isInstalled: false,
|
|
1194
|
+
reason: `Server error: ${error.message}`
|
|
1195
|
+
},
|
|
1196
|
+
mcpServer: {
|
|
1197
|
+
hasMCPServer: false,
|
|
1198
|
+
reason: `Server error: ${error.message}`
|
|
1199
|
+
},
|
|
1200
|
+
isReady: false
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* GET /api/taskmaster/detect/:projectName
|
|
1207
|
+
* Detect TaskMaster configuration for a specific project
|
|
1208
|
+
*/
|
|
1209
|
+
router.get('/detect/:projectName', async (req, res) => {
|
|
1210
|
+
try {
|
|
1211
|
+
const { projectName } = req.params;
|
|
1212
|
+
|
|
1213
|
+
// Use the existing extractProjectDirectory function to get actual project path
|
|
1214
|
+
let projectPath;
|
|
1215
|
+
try {
|
|
1216
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
console.error('Error extracting project directory:', error);
|
|
1219
|
+
return res.status(404).json({
|
|
1220
|
+
error: 'Project path not found',
|
|
1221
|
+
projectName,
|
|
1222
|
+
message: error.message
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// Verify the project path exists
|
|
1227
|
+
try {
|
|
1228
|
+
await fsPromises.access(projectPath, fs.constants.R_OK);
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
return res.status(404).json({
|
|
1231
|
+
error: 'Project path not accessible',
|
|
1232
|
+
projectPath,
|
|
1233
|
+
projectName,
|
|
1234
|
+
message: error.message
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// Run detection in parallel
|
|
1239
|
+
const [taskMasterResult, mcpResult] = await Promise.all([
|
|
1240
|
+
detectTaskMasterFolder(projectPath),
|
|
1241
|
+
detectTaskMasterMCPServer()
|
|
1242
|
+
]);
|
|
1243
|
+
|
|
1244
|
+
const status = computeTaskmasterStatus(taskMasterResult, mcpResult);
|
|
1245
|
+
|
|
1246
|
+
const responseData = {
|
|
1247
|
+
projectName,
|
|
1248
|
+
projectPath,
|
|
1249
|
+
status,
|
|
1250
|
+
taskmaster: taskMasterResult,
|
|
1251
|
+
mcp: mcpResult,
|
|
1252
|
+
timestamp: new Date().toISOString()
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
res.json(responseData);
|
|
1256
|
+
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
console.error('TaskMaster detection error:', error);
|
|
1259
|
+
res.status(500).json({
|
|
1260
|
+
error: 'Failed to detect TaskMaster configuration',
|
|
1261
|
+
message: error.message
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* GET /api/taskmaster/detect-all
|
|
1268
|
+
* Detect TaskMaster configuration for all known projects
|
|
1269
|
+
* This endpoint works with the existing projects system
|
|
1270
|
+
*/
|
|
1271
|
+
router.get('/detect-all', async (req, res) => {
|
|
1272
|
+
try {
|
|
1273
|
+
// Import getProjects from the projects module
|
|
1274
|
+
const { getProjects } = await import('../projects.js');
|
|
1275
|
+
const projects = await getProjects();
|
|
1276
|
+
|
|
1277
|
+
// Run detection for all projects in parallel
|
|
1278
|
+
const detectionPromises = projects.map(async (project) => {
|
|
1279
|
+
try {
|
|
1280
|
+
// Use the project's fullPath if available, otherwise extract the directory
|
|
1281
|
+
let projectPath;
|
|
1282
|
+
if (project.fullPath) {
|
|
1283
|
+
projectPath = project.fullPath;
|
|
1284
|
+
} else {
|
|
1285
|
+
try {
|
|
1286
|
+
projectPath = await extractProjectDirectory(project.name);
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
throw new Error(`Failed to extract project directory: ${error.message}`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const [taskMasterResult, mcpResult] = await Promise.all([
|
|
1293
|
+
detectTaskMasterFolder(projectPath),
|
|
1294
|
+
detectTaskMasterMCPServer()
|
|
1295
|
+
]);
|
|
1296
|
+
|
|
1297
|
+
const status = computeTaskmasterStatus(taskMasterResult, mcpResult);
|
|
1298
|
+
|
|
1299
|
+
return {
|
|
1300
|
+
projectName: project.name,
|
|
1301
|
+
displayName: project.displayName,
|
|
1302
|
+
projectPath,
|
|
1303
|
+
status,
|
|
1304
|
+
taskmaster: taskMasterResult,
|
|
1305
|
+
mcp: mcpResult
|
|
1306
|
+
};
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
return {
|
|
1309
|
+
projectName: project.name,
|
|
1310
|
+
displayName: project.displayName,
|
|
1311
|
+
status: 'error',
|
|
1312
|
+
error: error.message
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
const results = await Promise.all(detectionPromises);
|
|
1318
|
+
|
|
1319
|
+
res.json({
|
|
1320
|
+
projects: results,
|
|
1321
|
+
summary: {
|
|
1322
|
+
total: results.length,
|
|
1323
|
+
fullyConfigured: results.filter(p => p.status === 'fully-configured').length,
|
|
1324
|
+
taskmasterOnly: results.filter(p => p.status === 'taskmaster-only').length,
|
|
1325
|
+
mcpOnly: results.filter(p => p.status === 'mcp-only').length,
|
|
1326
|
+
notConfigured: results.filter(p => p.status === 'not-configured').length,
|
|
1327
|
+
errors: results.filter(p => p.status === 'error').length
|
|
1328
|
+
},
|
|
1329
|
+
timestamp: new Date().toISOString()
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
} catch (error) {
|
|
1333
|
+
console.error('Bulk TaskMaster detection error:', error);
|
|
1334
|
+
res.status(500).json({
|
|
1335
|
+
error: 'Failed to detect TaskMaster configuration for projects',
|
|
1336
|
+
message: error.message
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* POST /api/taskmaster/initialize/:projectName
|
|
1343
|
+
* Initialize TaskMaster in a project (placeholder for future CLI integration)
|
|
1344
|
+
*/
|
|
1345
|
+
router.post('/initialize/:projectName', async (req, res) => {
|
|
1346
|
+
try {
|
|
1347
|
+
const { projectName } = req.params;
|
|
1348
|
+
let projectPath;
|
|
1349
|
+
try {
|
|
1350
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
return res.status(404).json({
|
|
1353
|
+
error: 'Project not found',
|
|
1354
|
+
message: `Project "${projectName}" does not exist`
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
const paths = await ensurePipelineInitialized(projectPath);
|
|
1359
|
+
res.json({
|
|
1360
|
+
projectName,
|
|
1361
|
+
projectPath,
|
|
1362
|
+
pipelinePath: paths.root,
|
|
1363
|
+
message: 'Pipeline initialized successfully',
|
|
1364
|
+
timestamp: new Date().toISOString()
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
console.error('TaskMaster initialization error:', error);
|
|
1369
|
+
res.status(500).json({
|
|
1370
|
+
error: 'Failed to initialize TaskMaster',
|
|
1371
|
+
message: error.message
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* GET /api/taskmaster/next/:projectName
|
|
1378
|
+
* Get the next recommended task from local pipeline tasks
|
|
1379
|
+
*/
|
|
1380
|
+
router.get('/next/:projectName', async (req, res) => {
|
|
1381
|
+
try {
|
|
1382
|
+
const { projectName } = req.params;
|
|
1383
|
+
|
|
1384
|
+
// Get project path
|
|
1385
|
+
let projectPath;
|
|
1386
|
+
try {
|
|
1387
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
return res.status(404).json({
|
|
1390
|
+
error: 'Project not found',
|
|
1391
|
+
message: `Project "${projectName}" does not exist`
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const paths = await ensurePipelineInitialized(projectPath);
|
|
1396
|
+
const { tasks } = await readTasksFile(paths.tasksFile);
|
|
1397
|
+
const nextTask = tasks.find((task) => task.status === 'in-progress')
|
|
1398
|
+
|| tasks.find((task) => task.status === 'pending')
|
|
1399
|
+
|| null;
|
|
1400
|
+
|
|
1401
|
+
res.json({
|
|
1402
|
+
projectName,
|
|
1403
|
+
projectPath,
|
|
1404
|
+
nextTask,
|
|
1405
|
+
timestamp: new Date().toISOString(),
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
} catch (error) {
|
|
1409
|
+
console.error('TaskMaster next task error:', error);
|
|
1410
|
+
res.status(500).json({
|
|
1411
|
+
error: 'Failed to get next task',
|
|
1412
|
+
message: error.message
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* GET /api/taskmaster/next-guidance/:projectName
|
|
1419
|
+
* Get next actionable task with guidance metadata for Chat handoff
|
|
1420
|
+
*/
|
|
1421
|
+
router.get('/next-guidance/:projectName', async (req, res) => {
|
|
1422
|
+
try {
|
|
1423
|
+
const { projectName } = req.params;
|
|
1424
|
+
let projectPath;
|
|
1425
|
+
try {
|
|
1426
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
return res.status(404).json({
|
|
1429
|
+
error: 'Project not found',
|
|
1430
|
+
message: `Project "${projectName}" does not exist`,
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
const paths = await ensurePipelineInitialized(projectPath);
|
|
1435
|
+
const { tasks } = await readTasksFile(paths.tasksFile);
|
|
1436
|
+
const guidance = computeNextGuidance(tasks);
|
|
1437
|
+
|
|
1438
|
+
res.json({
|
|
1439
|
+
projectName,
|
|
1440
|
+
projectPath,
|
|
1441
|
+
nextTask: guidance.nextTask,
|
|
1442
|
+
guidance: {
|
|
1443
|
+
whyNext: guidance.whyNext,
|
|
1444
|
+
requiredInputs: guidance.requiredInputs,
|
|
1445
|
+
suggestedSkills: guidance.suggestedSkills,
|
|
1446
|
+
nextActionPrompt: guidance.nextActionPrompt,
|
|
1447
|
+
},
|
|
1448
|
+
timestamp: new Date().toISOString(),
|
|
1449
|
+
});
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
console.error('TaskMaster next-guidance error:', error);
|
|
1452
|
+
res.status(500).json({
|
|
1453
|
+
error: 'Failed to get next guidance',
|
|
1454
|
+
message: error.message,
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* GET /api/taskmaster/summary/:projectName
|
|
1461
|
+
* Build a compact TaskMaster summary for CLI / OpenClaw reporting
|
|
1462
|
+
*/
|
|
1463
|
+
router.get('/summary/:projectName', async (req, res) => {
|
|
1464
|
+
try {
|
|
1465
|
+
const { projectName } = req.params;
|
|
1466
|
+
|
|
1467
|
+
let projectPath;
|
|
1468
|
+
try {
|
|
1469
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
return res.status(404).json({
|
|
1472
|
+
error: 'Project not found',
|
|
1473
|
+
message: `Project "${projectName}" does not exist`,
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
const paths = await ensurePipelineInitialized(projectPath);
|
|
1478
|
+
const [taskMasterResult, mcpResult, tasksResult] = await Promise.all([
|
|
1479
|
+
detectTaskMasterFolder(projectPath),
|
|
1480
|
+
detectTaskMasterMCPServer(),
|
|
1481
|
+
readTasksFile(paths.tasksFile),
|
|
1482
|
+
]);
|
|
1483
|
+
|
|
1484
|
+
const tasks = tasksResult.tasks || [];
|
|
1485
|
+
const guidanceResult = computeNextGuidance(tasks);
|
|
1486
|
+
const status = computeTaskmasterStatus(taskMasterResult, mcpResult);
|
|
1487
|
+
|
|
1488
|
+
res.json(buildTaskmasterSummaryPayload({
|
|
1489
|
+
projectName,
|
|
1490
|
+
projectPath,
|
|
1491
|
+
status,
|
|
1492
|
+
tasks,
|
|
1493
|
+
nextTask: guidanceResult.nextTask,
|
|
1494
|
+
guidance: {
|
|
1495
|
+
whyNext: guidanceResult.whyNext,
|
|
1496
|
+
requiredInputs: guidanceResult.requiredInputs,
|
|
1497
|
+
suggestedSkills: guidanceResult.suggestedSkills,
|
|
1498
|
+
nextActionPrompt: guidanceResult.nextActionPrompt,
|
|
1499
|
+
},
|
|
1500
|
+
updatedAt: new Date().toISOString(),
|
|
1501
|
+
}));
|
|
1502
|
+
} catch (error) {
|
|
1503
|
+
console.error('TaskMaster summary error:', error);
|
|
1504
|
+
res.status(500).json({
|
|
1505
|
+
error: 'Failed to build TaskMaster summary',
|
|
1506
|
+
message: error.message,
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* GET /api/taskmaster/tasks/:projectName
|
|
1513
|
+
* Load actual tasks from .pipeline/tasks/tasks.json
|
|
1514
|
+
*/
|
|
1515
|
+
router.get('/tasks/:projectName', async (req, res) => {
|
|
1516
|
+
try {
|
|
1517
|
+
const { projectName } = req.params;
|
|
1518
|
+
|
|
1519
|
+
// Get project path
|
|
1520
|
+
let projectPath;
|
|
1521
|
+
try {
|
|
1522
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
return res.status(404).json({
|
|
1525
|
+
error: 'Project not found',
|
|
1526
|
+
message: `Project "${projectName}" does not exist`
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
const paths = await ensurePipelineInitialized(projectPath);
|
|
1531
|
+
const tasksFilePath = paths.tasksFile;
|
|
1532
|
+
|
|
1533
|
+
// Check if tasks file exists
|
|
1534
|
+
try {
|
|
1535
|
+
await fsPromises.access(tasksFilePath);
|
|
1536
|
+
} catch (error) {
|
|
1537
|
+
return res.json({
|
|
1538
|
+
projectName,
|
|
1539
|
+
tasks: [],
|
|
1540
|
+
message: 'No tasks.json file found'
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// Read and parse tasks file
|
|
1545
|
+
try {
|
|
1546
|
+
const { tasks: transformedTasks, currentTag } = await readTasksFile(tasksFilePath);
|
|
1547
|
+
|
|
1548
|
+
res.json({
|
|
1549
|
+
projectName,
|
|
1550
|
+
projectPath,
|
|
1551
|
+
tasks: transformedTasks,
|
|
1552
|
+
currentTag,
|
|
1553
|
+
totalTasks: transformedTasks.length,
|
|
1554
|
+
tasksByStatus: {
|
|
1555
|
+
pending: transformedTasks.filter(t => t.status === 'pending').length,
|
|
1556
|
+
'in-progress': transformedTasks.filter(t => t.status === 'in-progress').length,
|
|
1557
|
+
done: transformedTasks.filter(t => t.status === 'done').length,
|
|
1558
|
+
review: transformedTasks.filter(t => t.status === 'review').length,
|
|
1559
|
+
deferred: transformedTasks.filter(t => t.status === 'deferred').length,
|
|
1560
|
+
cancelled: transformedTasks.filter(t => t.status === 'cancelled').length
|
|
1561
|
+
},
|
|
1562
|
+
timestamp: new Date().toISOString()
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
} catch (parseError) {
|
|
1566
|
+
console.error('Failed to parse tasks.json:', parseError);
|
|
1567
|
+
return res.status(500).json({
|
|
1568
|
+
error: 'Failed to parse tasks file',
|
|
1569
|
+
message: parseError.message
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
console.error('TaskMaster tasks loading error:', error);
|
|
1575
|
+
res.status(500).json({
|
|
1576
|
+
error: 'Failed to load TaskMaster tasks',
|
|
1577
|
+
message: error.message
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
/**
|
|
1583
|
+
* GET /api/taskmaster/artifacts/:projectName
|
|
1584
|
+
* Summarize recent project artifacts for mobile/reporting workflows.
|
|
1585
|
+
*/
|
|
1586
|
+
router.get('/artifacts/:projectName', async (req, res) => {
|
|
1587
|
+
try {
|
|
1588
|
+
const { projectName } = req.params;
|
|
1589
|
+
|
|
1590
|
+
let projectPath;
|
|
1591
|
+
try {
|
|
1592
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
return res.status(404).json({
|
|
1595
|
+
error: 'Project not found',
|
|
1596
|
+
message: `Project "${projectName}" does not exist`
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
const artifacts = await summarizeProjectArtifacts(projectPath);
|
|
1601
|
+
const latest = artifacts.length > 0 ? artifacts[0] : null;
|
|
1602
|
+
|
|
1603
|
+
res.json({
|
|
1604
|
+
projectName,
|
|
1605
|
+
projectPath,
|
|
1606
|
+
artifacts,
|
|
1607
|
+
latestArtifact: latest,
|
|
1608
|
+
totalArtifacts: artifacts.length,
|
|
1609
|
+
timestamp: new Date().toISOString(),
|
|
1610
|
+
});
|
|
1611
|
+
} catch (error) {
|
|
1612
|
+
console.error('TaskMaster artifact summary error:', error);
|
|
1613
|
+
res.status(500).json({
|
|
1614
|
+
error: 'Failed to summarize project artifacts',
|
|
1615
|
+
message: error.message,
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
/**
|
|
1621
|
+
* GET /api/taskmaster/prd/:projectName
|
|
1622
|
+
* List all PRD files in the project's .pipeline/docs directory
|
|
1623
|
+
*/
|
|
1624
|
+
router.get('/prd/:projectName', async (req, res) => {
|
|
1625
|
+
try {
|
|
1626
|
+
const { projectName } = req.params;
|
|
1627
|
+
|
|
1628
|
+
// Get project path
|
|
1629
|
+
let projectPath;
|
|
1630
|
+
try {
|
|
1631
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
1632
|
+
} catch (error) {
|
|
1633
|
+
return res.status(404).json({
|
|
1634
|
+
error: 'Project not found',
|
|
1635
|
+
message: `Project "${projectName}" does not exist`
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
const paths = await ensurePipelineInitialized(projectPath);
|
|
1640
|
+
const docsPath = paths.docsDir;
|
|
1641
|
+
|
|
1642
|
+
// Check if docs directory exists
|
|
1643
|
+
try {
|
|
1644
|
+
await fsPromises.access(docsPath, fs.constants.R_OK);
|
|
1645
|
+
} catch (error) {
|
|
1646
|
+
return res.json({
|
|
1647
|
+
projectName,
|
|
1648
|
+
prdFiles: [],
|
|
1649
|
+
message: 'No .pipeline/docs directory found'
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// Read directory and filter for PRD files
|
|
1654
|
+
try {
|
|
1655
|
+
const files = await fsPromises.readdir(docsPath);
|
|
1656
|
+
const prdFiles = [];
|
|
1657
|
+
|
|
1658
|
+
for (const file of files) {
|
|
1659
|
+
const filePath = path.join(docsPath, file);
|
|
1660
|
+
const stats = await fsPromises.stat(filePath);
|
|
1661
|
+
|
|
1662
|
+
if (stats.isFile() && (file.endsWith('.txt') || file.endsWith('.md') || file.endsWith('.json'))) {
|
|
1663
|
+
prdFiles.push({
|
|
1664
|
+
name: file,
|
|
1665
|
+
path: path.relative(projectPath, filePath),
|
|
1666
|
+
size: stats.size,
|
|
1667
|
+
modified: stats.mtime.toISOString(),
|
|
1668
|
+
created: stats.birthtime.toISOString()
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
res.json({
|
|
1674
|
+
projectName,
|
|
1675
|
+
projectPath,
|
|
1676
|
+
prdFiles: prdFiles.sort((a, b) => new Date(b.modified) - new Date(a.modified)),
|
|
1677
|
+
timestamp: new Date().toISOString()
|
|
1678
|
+
});
|
|
1679
|
+
|
|
1680
|
+
} catch (readError) {
|
|
1681
|
+
console.error('Error reading docs directory:', readError);
|
|
1682
|
+
return res.status(500).json({
|
|
1683
|
+
error: 'Failed to read PRD files',
|
|
1684
|
+
message: readError.message
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
} catch (error) {
|
|
1689
|
+
console.error('PRD list error:', error);
|
|
1690
|
+
res.status(500).json({
|
|
1691
|
+
error: 'Failed to list PRD files',
|
|
1692
|
+
message: error.message
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* POST /api/taskmaster/prd/:projectName
|
|
1699
|
+
* Create or update a PRD file in the project's .pipeline/docs directory
|
|
1700
|
+
*/
|
|
1701
|
+
router.post('/prd/:projectName', async (req, res) => {
|
|
1702
|
+
try {
|
|
1703
|
+
const { projectName } = req.params;
|
|
1704
|
+
const { fileName, content } = req.body;
|
|
1705
|
+
|
|
1706
|
+
if (!fileName || !content) {
|
|
1707
|
+
return res.status(400).json({
|
|
1708
|
+
error: 'Missing required fields',
|
|
1709
|
+
message: 'fileName and content are required'
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// Validate filename
|
|
1714
|
+
if (!fileName.match(/^[\w\-. ]+\.(txt|md|json)$/)) {
|
|
1715
|
+
return res.status(400).json({
|
|
1716
|
+
error: 'Invalid filename',
|
|
1717
|
+
message: 'Filename must end with .txt, .md, or .json and contain only alphanumeric characters, spaces, dots, and dashes'
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// Get project path
|
|
1722
|
+
let projectPath;
|
|
1723
|
+
try {
|
|
1724
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
1725
|
+
} catch (error) {
|
|
1726
|
+
return res.status(404).json({
|
|
1727
|
+
error: 'Project not found',
|
|
1728
|
+
message: `Project "${projectName}" does not exist`
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
const paths = await ensurePipelineInitialized(projectPath);
|
|
1733
|
+
const docsPath = paths.docsDir;
|
|
1734
|
+
const filePath = path.join(docsPath, fileName);
|
|
1735
|
+
|
|
1736
|
+
// Ensure docs directory exists
|
|
1737
|
+
try {
|
|
1738
|
+
await fsPromises.mkdir(docsPath, { recursive: true });
|
|
1739
|
+
} catch (error) {
|
|
1740
|
+
console.error('Failed to create docs directory:', error);
|
|
1741
|
+
return res.status(500).json({
|
|
1742
|
+
error: 'Failed to create directory',
|
|
1743
|
+
message: error.message
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// Write the PRD file
|
|
1748
|
+
try {
|
|
1749
|
+
await fsPromises.writeFile(filePath, content, 'utf8');
|
|
1750
|
+
|
|
1751
|
+
// Get file stats
|
|
1752
|
+
const stats = await fsPromises.stat(filePath);
|
|
1753
|
+
|
|
1754
|
+
res.json({
|
|
1755
|
+
projectName,
|
|
1756
|
+
projectPath,
|
|
1757
|
+
fileName,
|
|
1758
|
+
filePath: path.relative(projectPath, filePath),
|
|
1759
|
+
size: stats.size,
|
|
1760
|
+
created: stats.birthtime.toISOString(),
|
|
1761
|
+
modified: stats.mtime.toISOString(),
|
|
1762
|
+
message: 'PRD file saved successfully',
|
|
1763
|
+
timestamp: new Date().toISOString()
|
|
1764
|
+
});
|
|
1765
|
+
|
|
1766
|
+
} catch (writeError) {
|
|
1767
|
+
console.error('Failed to write PRD file:', writeError);
|
|
1768
|
+
return res.status(500).json({
|
|
1769
|
+
error: 'Failed to write PRD file',
|
|
1770
|
+
message: writeError.message
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
} catch (error) {
|
|
1775
|
+
console.error('PRD create/update error:', error);
|
|
1776
|
+
res.status(500).json({
|
|
1777
|
+
error: 'Failed to create/update PRD file',
|
|
1778
|
+
message: error.message
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* GET /api/taskmaster/prd/:projectName/:fileName
|
|
1785
|
+
* Get content of a specific PRD file
|
|
1786
|
+
*/
|
|
1787
|
+
router.get('/prd/:projectName/:fileName', async (req, res) => {
|
|
1788
|
+
try {
|
|
1789
|
+
const { projectName, fileName } = req.params;
|
|
1790
|
+
|
|
1791
|
+
// Get project path
|
|
1792
|
+
let projectPath;
|
|
1793
|
+
try {
|
|
1794
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
1795
|
+
} catch (error) {
|
|
1796
|
+
return res.status(404).json({
|
|
1797
|
+
error: 'Project not found',
|
|
1798
|
+
message: `Project "${projectName}" does not exist`
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
const paths = await ensurePipelineInitialized(projectPath);
|
|
1803
|
+
const filePath = path.join(paths.docsDir, fileName);
|
|
1804
|
+
|
|
1805
|
+
// Check if file exists
|
|
1806
|
+
try {
|
|
1807
|
+
await fsPromises.access(filePath, fs.constants.R_OK);
|
|
1808
|
+
} catch (error) {
|
|
1809
|
+
return res.status(404).json({
|
|
1810
|
+
error: 'PRD file not found',
|
|
1811
|
+
message: `File "${fileName}" does not exist`
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// Read file content
|
|
1816
|
+
try {
|
|
1817
|
+
const content = await fsPromises.readFile(filePath, 'utf8');
|
|
1818
|
+
const stats = await fsPromises.stat(filePath);
|
|
1819
|
+
|
|
1820
|
+
res.json({
|
|
1821
|
+
projectName,
|
|
1822
|
+
projectPath,
|
|
1823
|
+
fileName,
|
|
1824
|
+
filePath: path.relative(projectPath, filePath),
|
|
1825
|
+
content,
|
|
1826
|
+
size: stats.size,
|
|
1827
|
+
created: stats.birthtime.toISOString(),
|
|
1828
|
+
modified: stats.mtime.toISOString(),
|
|
1829
|
+
timestamp: new Date().toISOString()
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
} catch (readError) {
|
|
1833
|
+
console.error('Failed to read PRD file:', readError);
|
|
1834
|
+
return res.status(500).json({
|
|
1835
|
+
error: 'Failed to read PRD file',
|
|
1836
|
+
message: readError.message
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
console.error('PRD read error:', error);
|
|
1842
|
+
res.status(500).json({
|
|
1843
|
+
error: 'Failed to read PRD file',
|
|
1844
|
+
message: error.message
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1849
|
+
/**
|
|
1850
|
+
* DELETE /api/taskmaster/prd/:projectName/:fileName
|
|
1851
|
+
* Delete a specific PRD file
|
|
1852
|
+
*/
|
|
1853
|
+
router.delete('/prd/:projectName/:fileName', async (req, res) => {
|
|
1854
|
+
try {
|
|
1855
|
+
const { projectName, fileName } = req.params;
|
|
1856
|
+
|
|
1857
|
+
// Get project path
|
|
1858
|
+
let projectPath;
|
|
1859
|
+
try {
|
|
1860
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
1861
|
+
} catch (error) {
|
|
1862
|
+
return res.status(404).json({
|
|
1863
|
+
error: 'Project not found',
|
|
1864
|
+
message: `Project "${projectName}" does not exist`
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
const paths = await ensurePipelineInitialized(projectPath);
|
|
1869
|
+
const filePath = path.join(paths.docsDir, fileName);
|
|
1870
|
+
|
|
1871
|
+
// Check if file exists
|
|
1872
|
+
try {
|
|
1873
|
+
await fsPromises.access(filePath, fs.constants.F_OK);
|
|
1874
|
+
} catch (error) {
|
|
1875
|
+
return res.status(404).json({
|
|
1876
|
+
error: 'PRD file not found',
|
|
1877
|
+
message: `File "${fileName}" does not exist`
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// Delete the file
|
|
1882
|
+
try {
|
|
1883
|
+
await fsPromises.unlink(filePath);
|
|
1884
|
+
|
|
1885
|
+
res.json({
|
|
1886
|
+
projectName,
|
|
1887
|
+
projectPath,
|
|
1888
|
+
fileName,
|
|
1889
|
+
message: 'PRD file deleted successfully',
|
|
1890
|
+
timestamp: new Date().toISOString()
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
} catch (deleteError) {
|
|
1894
|
+
console.error('Failed to delete PRD file:', deleteError);
|
|
1895
|
+
return res.status(500).json({
|
|
1896
|
+
error: 'Failed to delete PRD file',
|
|
1897
|
+
message: deleteError.message
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
} catch (error) {
|
|
1902
|
+
console.error('PRD delete error:', error);
|
|
1903
|
+
res.status(500).json({
|
|
1904
|
+
error: 'Failed to delete PRD file',
|
|
1905
|
+
message: error.message
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
/**
|
|
1911
|
+
* POST /api/taskmaster/init/:projectName
|
|
1912
|
+
* Initialize TaskMaster in a project
|
|
1913
|
+
*/
|
|
1914
|
+
router.post('/init/:projectName', async (req, res) => {
|
|
1915
|
+
try {
|
|
1916
|
+
const { projectName } = req.params;
|
|
1917
|
+
|
|
1918
|
+
// Get project path
|
|
1919
|
+
let projectPath;
|
|
1920
|
+
try {
|
|
1921
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
1922
|
+
} catch (error) {
|
|
1923
|
+
return res.status(404).json({
|
|
1924
|
+
error: 'Project not found',
|
|
1925
|
+
message: `Project "${projectName}" does not exist`
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
const paths = await ensurePipelineInitialized(projectPath);
|
|
1930
|
+
if (req.app.locals.wss) {
|
|
1931
|
+
broadcastTaskMasterProjectUpdate(
|
|
1932
|
+
req.app.locals.wss,
|
|
1933
|
+
projectName,
|
|
1934
|
+
{ hasTaskmaster: true, status: 'initialized', path: paths.root }
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
res.json({
|
|
1939
|
+
projectName,
|
|
1940
|
+
projectPath,
|
|
1941
|
+
pipelinePath: paths.root,
|
|
1942
|
+
message: 'Pipeline initialized successfully',
|
|
1943
|
+
timestamp: new Date().toISOString(),
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
} catch (error) {
|
|
1947
|
+
console.error('TaskMaster init error:', error);
|
|
1948
|
+
res.status(500).json({
|
|
1949
|
+
error: 'Failed to initialize TaskMaster',
|
|
1950
|
+
message: error.message
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
/**
|
|
1956
|
+
* POST /api/taskmaster/add-task/:projectName
|
|
1957
|
+
* Add a new task to the project
|
|
1958
|
+
*/
|
|
1959
|
+
router.post('/add-task/:projectName', async (req, res) => {
|
|
1960
|
+
try {
|
|
1961
|
+
const { projectName } = req.params;
|
|
1962
|
+
const { prompt, title, description, priority = 'high', dependencies, stage, insertAfterId } = req.body;
|
|
1963
|
+
|
|
1964
|
+
if (!prompt && (!title || !description)) {
|
|
1965
|
+
return res.status(400).json({
|
|
1966
|
+
error: 'Missing required parameters',
|
|
1967
|
+
message: 'Either "prompt" or both "title" and "description" are required'
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// Get project path
|
|
1972
|
+
let projectPath;
|
|
1973
|
+
try {
|
|
1974
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
return res.status(404).json({
|
|
1977
|
+
error: 'Project not found',
|
|
1978
|
+
message: `Project "${projectName}" does not exist`
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
const paths = await ensurePipelineInitialized(projectPath);
|
|
1983
|
+
const { tasks, currentTag } = await readTasksFile(paths.tasksFile);
|
|
1984
|
+
const taskId = generateTaskId(tasks);
|
|
1985
|
+
const now = new Date().toISOString();
|
|
1986
|
+
|
|
1987
|
+
const dependencyList = Array.isArray(dependencies)
|
|
1988
|
+
? dependencies
|
|
1989
|
+
: typeof dependencies === 'string' && dependencies.trim().length > 0
|
|
1990
|
+
? dependencies.split(',').map((item) => item.trim()).filter(Boolean)
|
|
1991
|
+
: [];
|
|
1992
|
+
|
|
1993
|
+
const newTask = normalizeTask({
|
|
1994
|
+
id: taskId,
|
|
1995
|
+
title: title || splitPromptToTitle(prompt),
|
|
1996
|
+
description: description || (prompt ? String(prompt).trim() : ''),
|
|
1997
|
+
priority,
|
|
1998
|
+
status: 'pending',
|
|
1999
|
+
dependencies: dependencyList,
|
|
2000
|
+
...(stage ? { stage } : {}),
|
|
2001
|
+
createdAt: now,
|
|
2002
|
+
updatedAt: now,
|
|
2003
|
+
});
|
|
2004
|
+
|
|
2005
|
+
// Determine insertion position
|
|
2006
|
+
let nextTasks;
|
|
2007
|
+
if (insertAfterId !== undefined) {
|
|
2008
|
+
if (insertAfterId === null || insertAfterId === 0) {
|
|
2009
|
+
// Insert at beginning of the target stage
|
|
2010
|
+
const targetStage = normalizeStageName(stage);
|
|
2011
|
+
const firstInStageIdx = tasks.findIndex(
|
|
2012
|
+
(t) => normalizeStageName(t.stage) === targetStage
|
|
2013
|
+
);
|
|
2014
|
+
if (firstInStageIdx === -1) {
|
|
2015
|
+
nextTasks = [...tasks, newTask];
|
|
2016
|
+
} else {
|
|
2017
|
+
nextTasks = [...tasks.slice(0, firstInStageIdx), newTask, ...tasks.slice(firstInStageIdx)];
|
|
2018
|
+
}
|
|
2019
|
+
} else {
|
|
2020
|
+
// Insert after the specified task
|
|
2021
|
+
const afterIdx = tasks.findIndex((t) => String(t.id) === String(insertAfterId));
|
|
2022
|
+
if (afterIdx === -1) {
|
|
2023
|
+
nextTasks = [...tasks, newTask];
|
|
2024
|
+
} else {
|
|
2025
|
+
nextTasks = [...tasks.slice(0, afterIdx + 1), newTask, ...tasks.slice(afterIdx + 1)];
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
} else {
|
|
2029
|
+
nextTasks = [...tasks, newTask];
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// Reindex all tasks so IDs are sequential
|
|
2033
|
+
const reindexed = reindexTasks(nextTasks);
|
|
2034
|
+
await writeTasksFile(paths.tasksFile, reindexed, currentTag);
|
|
2035
|
+
|
|
2036
|
+
// Find the inserted task by its createdAt timestamp
|
|
2037
|
+
const insertedTask = reindexed.find((t) => t.createdAt === now && t.title === newTask.title);
|
|
2038
|
+
|
|
2039
|
+
if (req.app.locals.wss) {
|
|
2040
|
+
broadcastTaskMasterTasksUpdate(req.app.locals.wss, projectName);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
res.json({
|
|
2044
|
+
projectName,
|
|
2045
|
+
projectPath,
|
|
2046
|
+
message: 'Task added successfully',
|
|
2047
|
+
task: insertedTask || newTask,
|
|
2048
|
+
timestamp: now,
|
|
2049
|
+
});
|
|
2050
|
+
|
|
2051
|
+
} catch (error) {
|
|
2052
|
+
console.error('Add task error:', error);
|
|
2053
|
+
res.status(500).json({
|
|
2054
|
+
error: 'Failed to add task',
|
|
2055
|
+
message: error.message
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
/**
|
|
2061
|
+
* DELETE /api/taskmaster/delete-task/:projectName/:taskId
|
|
2062
|
+
* Permanently remove a task from the project
|
|
2063
|
+
*/
|
|
2064
|
+
router.delete('/delete-task/:projectName/:taskId', async (req, res) => {
|
|
2065
|
+
try {
|
|
2066
|
+
const { projectName, taskId } = req.params;
|
|
2067
|
+
|
|
2068
|
+
let projectPath;
|
|
2069
|
+
try {
|
|
2070
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
2071
|
+
} catch (error) {
|
|
2072
|
+
return res.status(404).json({
|
|
2073
|
+
error: 'Project not found',
|
|
2074
|
+
message: `Project "${projectName}" does not exist`
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
const paths = await ensurePipelineInitialized(projectPath);
|
|
2079
|
+
const { tasks, currentTag } = await readTasksFile(paths.tasksFile);
|
|
2080
|
+
|
|
2081
|
+
const targetId = String(taskId);
|
|
2082
|
+
const taskIndex = tasks.findIndex((t) => String(t.id) === targetId);
|
|
2083
|
+
if (taskIndex === -1) {
|
|
2084
|
+
return res.status(404).json({
|
|
2085
|
+
error: 'Task not found',
|
|
2086
|
+
message: `Task with ID "${taskId}" does not exist`
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
const deletedTask = tasks[taskIndex];
|
|
2091
|
+
const nextTasks = tasks.filter((_, i) => i !== taskIndex);
|
|
2092
|
+
|
|
2093
|
+
// Remove deleted task ID from dependency arrays, then reindex
|
|
2094
|
+
nextTasks.forEach((t) => {
|
|
2095
|
+
if (Array.isArray(t.dependencies)) {
|
|
2096
|
+
t.dependencies = t.dependencies.filter(
|
|
2097
|
+
(dep) => String(dep) !== targetId
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
});
|
|
2101
|
+
|
|
2102
|
+
const reindexed = reindexTasks(nextTasks);
|
|
2103
|
+
await writeTasksFile(paths.tasksFile, reindexed, currentTag);
|
|
2104
|
+
|
|
2105
|
+
if (req.app.locals.wss) {
|
|
2106
|
+
broadcastTaskMasterTasksUpdate(req.app.locals.wss, projectName);
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
res.json({
|
|
2110
|
+
projectName,
|
|
2111
|
+
projectPath,
|
|
2112
|
+
message: 'Task deleted successfully',
|
|
2113
|
+
deletedTask,
|
|
2114
|
+
timestamp: new Date().toISOString(),
|
|
2115
|
+
});
|
|
2116
|
+
} catch (error) {
|
|
2117
|
+
console.error('Delete task error:', error);
|
|
2118
|
+
res.status(500).json({
|
|
2119
|
+
error: 'Failed to delete task',
|
|
2120
|
+
message: error.message
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
|
|
2125
|
+
/**
|
|
2126
|
+
* PUT /api/taskmaster/update-task/:projectName/:taskId
|
|
2127
|
+
* Update a specific task using TaskMaster CLI
|
|
2128
|
+
*/
|
|
2129
|
+
router.put('/update-task/:projectName/:taskId', async (req, res) => {
|
|
2130
|
+
try {
|
|
2131
|
+
const { projectName, taskId } = req.params;
|
|
2132
|
+
const { title, description, status, priority, details, testStrategy, dependencies } = req.body;
|
|
2133
|
+
|
|
2134
|
+
// Get project path
|
|
2135
|
+
let projectPath;
|
|
2136
|
+
try {
|
|
2137
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
2138
|
+
} catch (error) {
|
|
2139
|
+
return res.status(404).json({
|
|
2140
|
+
error: 'Project not found',
|
|
2141
|
+
message: `Project "${projectName}" does not exist`
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
const paths = await ensurePipelineInitialized(projectPath);
|
|
2146
|
+
const { tasks, currentTag } = await readTasksFile(paths.tasksFile);
|
|
2147
|
+
const now = new Date().toISOString();
|
|
2148
|
+
const targetId = String(taskId);
|
|
2149
|
+
const taskIndex = tasks.findIndex((task) => String(task.id) === targetId);
|
|
2150
|
+
|
|
2151
|
+
if (taskIndex === -1) {
|
|
2152
|
+
return res.status(404).json({
|
|
2153
|
+
error: 'Task not found',
|
|
2154
|
+
message: `Task "${taskId}" does not exist`,
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
const existingTask = tasks[taskIndex];
|
|
2159
|
+
const updatedTask = {
|
|
2160
|
+
...existingTask,
|
|
2161
|
+
...(title !== undefined ? { title } : {}),
|
|
2162
|
+
...(description !== undefined ? { description } : {}),
|
|
2163
|
+
...(status !== undefined ? { status } : {}),
|
|
2164
|
+
...(priority !== undefined ? { priority } : {}),
|
|
2165
|
+
...(details !== undefined ? { details } : {}),
|
|
2166
|
+
...(testStrategy !== undefined ? { testStrategy } : {}),
|
|
2167
|
+
...(dependencies !== undefined ? { dependencies: Array.isArray(dependencies) ? dependencies : [] } : {}),
|
|
2168
|
+
updatedAt: now,
|
|
2169
|
+
};
|
|
2170
|
+
|
|
2171
|
+
const nextTasks = [...tasks];
|
|
2172
|
+
nextTasks[taskIndex] = normalizeTask(updatedTask);
|
|
2173
|
+
await writeTasksFile(paths.tasksFile, nextTasks, currentTag);
|
|
2174
|
+
|
|
2175
|
+
if (req.app.locals.wss) {
|
|
2176
|
+
broadcastTaskMasterTasksUpdate(req.app.locals.wss, projectName);
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
res.json({
|
|
2180
|
+
projectName,
|
|
2181
|
+
projectPath,
|
|
2182
|
+
taskId,
|
|
2183
|
+
message: 'Task updated successfully',
|
|
2184
|
+
task: nextTasks[taskIndex],
|
|
2185
|
+
timestamp: now,
|
|
2186
|
+
});
|
|
2187
|
+
|
|
2188
|
+
} catch (error) {
|
|
2189
|
+
console.error('Update task error:', error);
|
|
2190
|
+
res.status(500).json({
|
|
2191
|
+
error: 'Failed to update task',
|
|
2192
|
+
message: error.message
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
|
|
2197
|
+
/**
|
|
2198
|
+
* POST /api/taskmaster/parse-prd/:projectName
|
|
2199
|
+
* Parse a Research Brief JSON file to generate tasks
|
|
2200
|
+
*/
|
|
2201
|
+
router.post('/parse-prd/:projectName', async (req, res) => {
|
|
2202
|
+
try {
|
|
2203
|
+
const { projectName } = req.params;
|
|
2204
|
+
const { fileName = DEFAULT_RESEARCH_BRIEF_FILENAME, numTasks, append = false } = req.body;
|
|
2205
|
+
|
|
2206
|
+
// Get project path
|
|
2207
|
+
let projectPath;
|
|
2208
|
+
try {
|
|
2209
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
2210
|
+
} catch (error) {
|
|
2211
|
+
return res.status(404).json({
|
|
2212
|
+
error: 'Project not found',
|
|
2213
|
+
message: `Project "${projectName}" does not exist`
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
const paths = await ensurePipelineInitialized(projectPath);
|
|
2218
|
+
const briefPath = path.join(paths.docsDir, fileName);
|
|
2219
|
+
|
|
2220
|
+
// Check if brief JSON file exists
|
|
2221
|
+
try {
|
|
2222
|
+
await fsPromises.access(briefPath, fs.constants.F_OK);
|
|
2223
|
+
} catch (error) {
|
|
2224
|
+
return res.status(404).json({
|
|
2225
|
+
error: 'Research Brief file not found',
|
|
2226
|
+
message: `File "${fileName}" does not exist in ${PIPELINE_DIR}/docs/`
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
if (!fileName.endsWith('.json')) {
|
|
2231
|
+
return res.status(400).json({
|
|
2232
|
+
error: 'Invalid brief format',
|
|
2233
|
+
message: 'Research Brief must be a .json file',
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
let briefData;
|
|
2238
|
+
try {
|
|
2239
|
+
const briefContent = await fsPromises.readFile(briefPath, 'utf8');
|
|
2240
|
+
briefData = JSON.parse(briefContent);
|
|
2241
|
+
} catch (parseError) {
|
|
2242
|
+
return res.status(400).json({
|
|
2243
|
+
error: 'Invalid Research Brief JSON',
|
|
2244
|
+
message: parseError.message,
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
const maxTasks = Number.isFinite(Number(numTasks)) && Number(numTasks) > 0 ? Number(numTasks) : DEFAULT_MAX_TASKS;
|
|
2249
|
+
|
|
2250
|
+
const { tasks: existingTasks, currentTag } = await readTasksFile(paths.tasksFile);
|
|
2251
|
+
const now = new Date().toISOString();
|
|
2252
|
+
|
|
2253
|
+
const pipelineGenerated = instantiatePipelineTasksFromBrief(briefData, maxTasks);
|
|
2254
|
+
let generatedTasks = [];
|
|
2255
|
+
|
|
2256
|
+
if (pipelineGenerated && pipelineGenerated.length > 0) {
|
|
2257
|
+
let nextId = generateTaskId(existingTasks);
|
|
2258
|
+
generatedTasks = pipelineGenerated.map((task) => normalizeTask({
|
|
2259
|
+
...task,
|
|
2260
|
+
id: nextId++,
|
|
2261
|
+
createdAt: task.createdAt || now,
|
|
2262
|
+
updatedAt: now,
|
|
2263
|
+
}));
|
|
2264
|
+
} else {
|
|
2265
|
+
const candidates = parseBriefJsonToTaskCandidates(briefData);
|
|
2266
|
+
let nextId = generateTaskId(existingTasks);
|
|
2267
|
+
generatedTasks = candidates.slice(0, maxTasks).map((candidate) => normalizeTask({
|
|
2268
|
+
id: nextId++,
|
|
2269
|
+
title: splitPromptToTitle(candidate),
|
|
2270
|
+
description: candidate,
|
|
2271
|
+
status: 'pending',
|
|
2272
|
+
priority: 'medium',
|
|
2273
|
+
dependencies: [],
|
|
2274
|
+
createdAt: now,
|
|
2275
|
+
updatedAt: now,
|
|
2276
|
+
stage: inferStageFromCandidate(candidate),
|
|
2277
|
+
taskType: 'exploration',
|
|
2278
|
+
suggestedSkills: ensureArray(getStageSkillMap()[inferStageFromCandidate(candidate)]?.base),
|
|
2279
|
+
sourceBlueprintId: `legacy.candidate.${nextId - 1}`,
|
|
2280
|
+
nextActionPrompt: [
|
|
2281
|
+
`Task: ${splitPromptToTitle(candidate)}`,
|
|
2282
|
+
`Description: ${candidate}`,
|
|
2283
|
+
'Please turn this into a concrete actionable plan and provide first-step outputs.',
|
|
2284
|
+
].join('\n'),
|
|
2285
|
+
}));
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
const nextTasks = append
|
|
2289
|
+
? [...existingTasks, ...dedupeGeneratedTasks(existingTasks, generatedTasks)]
|
|
2290
|
+
: generatedTasks;
|
|
2291
|
+
await writeTasksFile(paths.tasksFile, nextTasks, currentTag);
|
|
2292
|
+
|
|
2293
|
+
if (req.app.locals.wss) {
|
|
2294
|
+
broadcastTaskMasterTasksUpdate(req.app.locals.wss, projectName);
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
res.json({
|
|
2298
|
+
projectName,
|
|
2299
|
+
projectPath,
|
|
2300
|
+
briefFile: fileName,
|
|
2301
|
+
generationMode: pipelineGenerated ? 'pipeline-blueprint' : 'legacy-fallback',
|
|
2302
|
+
message: pipelineGenerated
|
|
2303
|
+
? 'Research Brief pipeline instantiated successfully'
|
|
2304
|
+
: 'Research Brief parsed and tasks generated successfully',
|
|
2305
|
+
generatedCount: generatedTasks.length,
|
|
2306
|
+
totalTasks: nextTasks.length,
|
|
2307
|
+
timestamp: now,
|
|
2308
|
+
});
|
|
2309
|
+
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
console.error('Parse PRD error:', error);
|
|
2312
|
+
res.status(500).json({
|
|
2313
|
+
error: 'Failed to parse Research Brief',
|
|
2314
|
+
message: error.message
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
2318
|
+
|
|
2319
|
+
/**
|
|
2320
|
+
* GET /api/taskmaster/prd-templates
|
|
2321
|
+
* Get available PRD templates
|
|
2322
|
+
*/
|
|
2323
|
+
router.get('/prd-templates', async (req, res) => {
|
|
2324
|
+
try {
|
|
2325
|
+
const templates = await getAvailableTemplates();
|
|
2326
|
+
res.json({
|
|
2327
|
+
templates,
|
|
2328
|
+
timestamp: new Date().toISOString()
|
|
2329
|
+
});
|
|
2330
|
+
|
|
2331
|
+
} catch (error) {
|
|
2332
|
+
console.error('PRD templates error:', error);
|
|
2333
|
+
res.status(500).json({
|
|
2334
|
+
error: 'Failed to get PRD templates',
|
|
2335
|
+
message: error.message
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
});
|
|
2339
|
+
|
|
2340
|
+
function normalizeLoadedTemplate(template = {}) {
|
|
2341
|
+
return {
|
|
2342
|
+
...template,
|
|
2343
|
+
category: template.category || template.domain || 'general',
|
|
2344
|
+
format: template.format || 'research-brief-json',
|
|
2345
|
+
fileName: template.fileName || DEFAULT_RESEARCH_BRIEF_FILENAME,
|
|
2346
|
+
metaFields: Array.isArray(template.metaFields) ? template.metaFields : [],
|
|
2347
|
+
sectionFields: template.sectionFields && typeof template.sectionFields === 'object' ? template.sectionFields : {},
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
function cloneJsonCompatible(value) {
|
|
2352
|
+
return JSON.parse(JSON.stringify(value));
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
async function summarizeProjectArtifacts(projectPath, limit = 12) {
|
|
2356
|
+
const candidates = [
|
|
2357
|
+
'.pipeline/docs',
|
|
2358
|
+
'.pipeline/tasks',
|
|
2359
|
+
'results',
|
|
2360
|
+
'reports',
|
|
2361
|
+
'artifacts',
|
|
2362
|
+
'output',
|
|
2363
|
+
'outputs',
|
|
2364
|
+
'analysis',
|
|
2365
|
+
'figures',
|
|
2366
|
+
'plots',
|
|
2367
|
+
'tables',
|
|
2368
|
+
'paper',
|
|
2369
|
+
'drafts',
|
|
2370
|
+
];
|
|
2371
|
+
|
|
2372
|
+
const artifactFiles = [];
|
|
2373
|
+
for (const relativeDir of candidates) {
|
|
2374
|
+
const targetDir = path.join(projectPath, relativeDir);
|
|
2375
|
+
try {
|
|
2376
|
+
const entries = await fsPromises.readdir(targetDir, { withFileTypes: true });
|
|
2377
|
+
for (const entry of entries) {
|
|
2378
|
+
if (!entry.isFile()) {
|
|
2379
|
+
continue;
|
|
2380
|
+
}
|
|
2381
|
+
const fullPath = path.join(targetDir, entry.name);
|
|
2382
|
+
const stats = await fsPromises.stat(fullPath);
|
|
2383
|
+
artifactFiles.push({
|
|
2384
|
+
name: entry.name,
|
|
2385
|
+
relativePath: path.relative(projectPath, fullPath),
|
|
2386
|
+
category: relativeDir,
|
|
2387
|
+
size: stats.size,
|
|
2388
|
+
modified: stats.mtime.toISOString(),
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
if (error?.code !== 'ENOENT') {
|
|
2393
|
+
console.warn('[TaskMaster] Artifact scan skipped for', targetDir, error.message);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
artifactFiles.sort((a, b) => new Date(b.modified) - new Date(a.modified));
|
|
2399
|
+
return artifactFiles.slice(0, limit);
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
function buildBriefFromTemplate(template, nowDate) {
|
|
2403
|
+
const stageSkillMap = getStageSkillMap();
|
|
2404
|
+
return {
|
|
2405
|
+
schemaVersion: '1.1',
|
|
2406
|
+
templateId: template.id,
|
|
2407
|
+
meta: {
|
|
2408
|
+
title: '',
|
|
2409
|
+
lead_author: '',
|
|
2410
|
+
target_venue: '',
|
|
2411
|
+
date: nowDate,
|
|
2412
|
+
},
|
|
2413
|
+
sections: cloneJsonCompatible(DEFAULT_BRIEF_SECTIONS),
|
|
2414
|
+
pipeline: cloneJsonCompatible(
|
|
2415
|
+
template?.pipeline && typeof template.pipeline === 'object'
|
|
2416
|
+
? template.pipeline
|
|
2417
|
+
: buildDefaultBriefPipeline(stageSkillMap),
|
|
2418
|
+
),
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
/**
|
|
2423
|
+
* POST /api/taskmaster/apply-template/:projectName
|
|
2424
|
+
* Apply a structured template to create/update a Research Brief JSON file
|
|
2425
|
+
*/
|
|
2426
|
+
router.post('/apply-template/:projectName', async (req, res) => {
|
|
2427
|
+
try {
|
|
2428
|
+
const { projectName } = req.params;
|
|
2429
|
+
const { templateId, fileName = DEFAULT_RESEARCH_BRIEF_FILENAME, customizations = {} } = req.body;
|
|
2430
|
+
|
|
2431
|
+
if (!templateId) {
|
|
2432
|
+
return res.status(400).json({
|
|
2433
|
+
error: 'Missing required parameter',
|
|
2434
|
+
message: 'templateId is required'
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
// Get project path
|
|
2439
|
+
let projectPath;
|
|
2440
|
+
try {
|
|
2441
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
2442
|
+
} catch (error) {
|
|
2443
|
+
return res.status(404).json({
|
|
2444
|
+
error: 'Project not found',
|
|
2445
|
+
message: `Project "${projectName}" does not exist`
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
const templates = await getAvailableTemplates();
|
|
2450
|
+
const template = templates.find(t => t.id === templateId);
|
|
2451
|
+
|
|
2452
|
+
if (!template) {
|
|
2453
|
+
return res.status(404).json({
|
|
2454
|
+
error: 'Template not found',
|
|
2455
|
+
message: `Template "${templateId}" does not exist`
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
if (!fileName.endsWith('.json')) {
|
|
2460
|
+
return res.status(400).json({
|
|
2461
|
+
error: 'Invalid filename',
|
|
2462
|
+
message: 'Research Brief must be saved as .json',
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
const now = new Date().toISOString().split('T')[0];
|
|
2467
|
+
const brief = buildBriefFromTemplate(template, now);
|
|
2468
|
+
|
|
2469
|
+
const allFields = [
|
|
2470
|
+
...(Array.isArray(template.metaFields) ? template.metaFields : []),
|
|
2471
|
+
...Object.values(template.sectionFields || {}).flat(),
|
|
2472
|
+
];
|
|
2473
|
+
|
|
2474
|
+
allFields.forEach((field) => {
|
|
2475
|
+
const submitted = customizations?.[field.path];
|
|
2476
|
+
if (submitted === undefined || submitted === null) return;
|
|
2477
|
+
const rawValue = typeof submitted === 'string' ? submitted.trim() : submitted;
|
|
2478
|
+
if (rawValue === '') return;
|
|
2479
|
+
|
|
2480
|
+
if (field.type === 'array') {
|
|
2481
|
+
const values = Array.isArray(rawValue)
|
|
2482
|
+
? rawValue.map((item) => String(item).trim()).filter(Boolean)
|
|
2483
|
+
: String(rawValue).split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
|
|
2484
|
+
assignPath(brief, field.path, values);
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
assignPath(brief, field.path, String(rawValue));
|
|
2489
|
+
});
|
|
2490
|
+
|
|
2491
|
+
const paths = await ensurePipelineInitialized(projectPath);
|
|
2492
|
+
const docsDir = paths.docsDir;
|
|
2493
|
+
try {
|
|
2494
|
+
await fsPromises.mkdir(docsDir, { recursive: true });
|
|
2495
|
+
} catch (error) {
|
|
2496
|
+
console.error('Failed to create docs directory:', error);
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
const filePath = path.join(docsDir, fileName);
|
|
2500
|
+
|
|
2501
|
+
// Write the template content to the file
|
|
2502
|
+
try {
|
|
2503
|
+
await fsPromises.writeFile(filePath, `${JSON.stringify(brief, null, 2)}\n`, 'utf8');
|
|
2504
|
+
|
|
2505
|
+
res.json({
|
|
2506
|
+
projectName,
|
|
2507
|
+
projectPath,
|
|
2508
|
+
templateId,
|
|
2509
|
+
templateName: template.name,
|
|
2510
|
+
fileName,
|
|
2511
|
+
filePath: path.relative(projectPath, filePath),
|
|
2512
|
+
message: 'Research Brief template applied successfully',
|
|
2513
|
+
timestamp: new Date().toISOString()
|
|
2514
|
+
});
|
|
2515
|
+
|
|
2516
|
+
} catch (writeError) {
|
|
2517
|
+
console.error('Failed to write PRD template:', writeError);
|
|
2518
|
+
return res.status(500).json({
|
|
2519
|
+
error: 'Failed to write Research Brief',
|
|
2520
|
+
message: writeError.message
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
} catch (error) {
|
|
2525
|
+
console.error('Apply template error:', error);
|
|
2526
|
+
res.status(500).json({
|
|
2527
|
+
error: 'Failed to apply Research Brief template',
|
|
2528
|
+
message: error.message
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
});
|
|
2532
|
+
|
|
2533
|
+
// Helper function to get available templates
|
|
2534
|
+
async function getAvailableTemplates() {
|
|
2535
|
+
if (cachedTemplates) {
|
|
2536
|
+
return cachedTemplates;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
let files = [];
|
|
2540
|
+
try {
|
|
2541
|
+
files = await fsPromises.readdir(TEMPLATES_DIR);
|
|
2542
|
+
} catch (error) {
|
|
2543
|
+
throw new Error(`Failed to read templates directory: ${error.message}`);
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
const jsonFiles = files.filter((name) => name.endsWith('.json'));
|
|
2547
|
+
if (jsonFiles.length === 0) {
|
|
2548
|
+
throw new Error(`No template JSON files found in ${TEMPLATES_DIR}`);
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
const loaded = [];
|
|
2552
|
+
for (const fileName of jsonFiles) {
|
|
2553
|
+
const filePath = path.join(TEMPLATES_DIR, fileName);
|
|
2554
|
+
const content = await fsPromises.readFile(filePath, 'utf8');
|
|
2555
|
+
const parsed = JSON.parse(content);
|
|
2556
|
+
if (!parsed?.id || !parsed?.name) {
|
|
2557
|
+
throw new Error(`Template "${fileName}" missing required fields: id/name`);
|
|
2558
|
+
}
|
|
2559
|
+
loaded.push(normalizeLoadedTemplate(parsed));
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
cachedTemplates = loaded.sort((a, b) => a.name.localeCompare(b.name));
|
|
2563
|
+
return cachedTemplates;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
export default router;
|