catalyst-core-internal 0.1.2 → 0.1.4
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/README.md +4 -4
- package/bin/catalyst.js +8 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +3 -11
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +12 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +18 -3
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +7 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +6 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +253 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/SecurityBridgeTest.kt +199 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +139 -0
- package/dist/native/bridge/hooks.js +4 -4
- package/dist/native/bridge/useBaseHook.js +5 -4
- package/dist/native/bridge/utils/NativeBridge.js +4 -4
- package/dist/native/buildAppAndroid.js +2 -2
- package/dist/native/buildAppIos.js +10 -17
- package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
- package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
- package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
- package/dist/native/internalPluginUtils.js +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +5 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +6 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +364 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +13 -2
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +13 -2
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +14 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +6 -0
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -0
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +36 -0
- package/dist/native/iosnativeWebView/iosnativeWebView.xctestplan +1 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeCommandHandlerSecurityTests.swift +212 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/FrameworkServerUtilsTests.swift +14 -4
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +160 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/ScreenSecureManagerTests.swift +121 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/WebViewTests.swift +9 -21
- package/dist/native/plugin-bridge/PluginBridge.js +1 -0
- package/dist/native/pluginComposerAndroid.js +9 -0
- package/dist/native/pluginComposerIos.js +7 -0
- package/dist/scripts/plugins.js +1 -0
- package/package.json +3 -2
- package/mcp_v2/conversion-tasks.json +0 -371
- package/mcp_v2/knowledge-base.json +0 -1450
- package/mcp_v2/lib/helpers.js +0 -145
- package/mcp_v2/mcp.js +0 -366
- package/mcp_v2/package.json +0 -13
- package/mcp_v2/schema.sql +0 -88
- package/mcp_v2/setup.js +0 -262
- package/mcp_v2/tools/build.js +0 -449
- package/mcp_v2/tools/config.js +0 -262
- package/mcp_v2/tools/conversion.js +0 -492
- package/mcp_v2/tools/debug.js +0 -62
- package/mcp_v2/tools/knowledge.js +0 -213
- package/mcp_v2/tools/sync.js +0 -21
- package/mcp_v2/tools/tasks.js +0 -844
package/mcp_v2/tools/tasks.js
DELETED
|
@@ -1,844 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { findCatalystRoot } = require('../lib/helpers');
|
|
6
|
-
const conversion = require('./conversion');
|
|
7
|
-
|
|
8
|
-
let _db;
|
|
9
|
-
|
|
10
|
-
function init(db) {
|
|
11
|
-
_db = db;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
function slugify(str) {
|
|
17
|
-
return str
|
|
18
|
-
.toLowerCase()
|
|
19
|
-
.replace(/[^a-z0-9\s-]/g, '')
|
|
20
|
-
.trim()
|
|
21
|
-
.replace(/\s+/g, '-')
|
|
22
|
-
.slice(0, 60);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function now() {
|
|
26
|
-
return new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// ─── Improvement 1: Resolve needs_review inline before plan write ─────────────
|
|
30
|
-
//
|
|
31
|
-
// Reads signal_files from disk for each needs_review task.
|
|
32
|
-
// Returns { verdict: 'gap'|'completed', evidence } so the plan step is final.
|
|
33
|
-
// No needs_review items survive into the plan.
|
|
34
|
-
|
|
35
|
-
function resolveNeedsReview(task, projectRoot) {
|
|
36
|
-
const rc = task.review_context;
|
|
37
|
-
if (!rc || !rc.signal_files) {
|
|
38
|
-
// No signal files to read — treat as gap (conservative)
|
|
39
|
-
return { verdict: 'gap', evidence: 'No signal files available. Treating as gap to be safe.' };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const sf = rc.signal_files;
|
|
43
|
-
|
|
44
|
-
// Collect all file lists from signal_files object
|
|
45
|
-
const oldFiles = sf.old_pattern_files || [];
|
|
46
|
-
const hookFiles = sf.hook_usage_files || [];
|
|
47
|
-
const fetcherFiles = sf.converted_files || [];
|
|
48
|
-
const correctFiles = sf.correct_pattern_files || [];
|
|
49
|
-
|
|
50
|
-
// Read a sample of the old-pattern files (up to 3) to check for hook presence
|
|
51
|
-
const samplesToCheck = oldFiles.slice(0, 3);
|
|
52
|
-
const hookNames = {
|
|
53
|
-
T4_DATA_FETCHING: /serverFetcher|clientFetcher/,
|
|
54
|
-
T17a_USE_FILEPICKER: /useFilePicker/,
|
|
55
|
-
T17b_USE_CAMERA: /useCamera/,
|
|
56
|
-
T18_USE_HAPTIC: /useHapticFeedback/,
|
|
57
|
-
T20_USE_DEVICE_INFO: /__PLATFORM__|window\.__PLATFORM__/,
|
|
58
|
-
};
|
|
59
|
-
const hookRegex = hookNames[task.id];
|
|
60
|
-
|
|
61
|
-
// If the task has hook_usage_files or correct_pattern_files already found → leaning completed
|
|
62
|
-
const mitigationFiles = hookFiles.length ? hookFiles : correctFiles;
|
|
63
|
-
|
|
64
|
-
if (mitigationFiles.length > 0) {
|
|
65
|
-
// Hook/correct pattern IS present somewhere — check if it's in the same files
|
|
66
|
-
// or at minimum in the project. We read the old-pattern files to see if they
|
|
67
|
-
// import the hook directly, or accept it as a prop.
|
|
68
|
-
let nativeBranchFound = false;
|
|
69
|
-
for (const f of samplesToCheck) {
|
|
70
|
-
try {
|
|
71
|
-
const absPath = path.isAbsolute(f) ? f : path.join(projectRoot, f);
|
|
72
|
-
const content = fs.readFileSync(absPath, 'utf8');
|
|
73
|
-
if (hookRegex && hookRegex.test(content)) {
|
|
74
|
-
nativeBranchFound = true;
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
// Props pattern: isNative or execute passed as props
|
|
78
|
-
if (/isNative|execute\s*\(/.test(content)) {
|
|
79
|
-
nativeBranchFound = true;
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
} catch {
|
|
83
|
-
// file unreadable — skip
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (nativeBranchFound) {
|
|
88
|
-
return {
|
|
89
|
-
verdict: 'completed',
|
|
90
|
-
evidence: `Native branch confirmed in ${samplesToCheck.join(', ')}. Hook or isNative pattern found.`,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Hook exists elsewhere in project but NOT in the old-pattern files themselves.
|
|
95
|
-
// Component tree pass-through is possible but unconfirmed — treat as gap.
|
|
96
|
-
return {
|
|
97
|
-
verdict: 'gap',
|
|
98
|
-
evidence: `Hook found in ${mitigationFiles.slice(0,2).join(', ')} but not inside old-pattern files. ` +
|
|
99
|
-
`Likely unconverted: ${oldFiles.slice(0,3).join(', ')}.`,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// No mitigation files at all → definite gap
|
|
104
|
-
return {
|
|
105
|
-
verdict: 'gap',
|
|
106
|
-
evidence: `No mitigation found. ${oldFiles.length} file(s) with old pattern: ${oldFiles.slice(0,3).join(', ')}.`,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// ─── Improvement 2: Derive files_to_touch + missing_items per step ────────────
|
|
111
|
-
//
|
|
112
|
-
// For gap steps:
|
|
113
|
-
// - files_to_touch: from gap.files (already collected by detector)
|
|
114
|
-
// - missing_items: for config tasks → missing config keys; for file-absence tasks → list missing files
|
|
115
|
-
// For resolved needs_review steps (verdict=gap):
|
|
116
|
-
// - files_to_touch: from signal_files.old_pattern_files
|
|
117
|
-
// - missing_items: describe what needs to change in each file
|
|
118
|
-
|
|
119
|
-
function deriveFilesTouched(task, projectRoot) {
|
|
120
|
-
// task here is a raw gap/needs_review item from get_conversion_status output
|
|
121
|
-
const out = { files_to_touch: [], missing_items: [] };
|
|
122
|
-
|
|
123
|
-
// From detector files array (T19_USE_NOTIFICATIONS, T13/T14 use this)
|
|
124
|
-
if (task.files && task.files.length) {
|
|
125
|
-
out.files_to_touch = task.files.slice(0, 10);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// From signal_files (needs_review-derived gaps)
|
|
129
|
-
if (task.review_context?.signal_files) {
|
|
130
|
-
const sf = task.review_context.signal_files;
|
|
131
|
-
const candidates = (sf.old_pattern_files || []).slice(0, 10);
|
|
132
|
-
out.files_to_touch = [...new Set([...out.files_to_touch, ...candidates])];
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Missing config keys (from reason string pattern "Missing fields: X, Y")
|
|
136
|
-
const missingFieldsMatch = (task.reason || '').match(/Missing(?:\s+(?:fields|Android icons|iOS icons|Firebase files|Splash asset files|server files|client files))?:\s*(.+)/i);
|
|
137
|
-
if (missingFieldsMatch) {
|
|
138
|
-
out.missing_items = missingFieldsMatch[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Files that don't exist yet (from reason string "not found" pattern)
|
|
142
|
-
const notFoundMatch = (task.reason || '').match(/^(.+)\s+not found$/i);
|
|
143
|
-
if (notFoundMatch && !out.files_to_touch.length) {
|
|
144
|
-
out.files_to_touch = [notFoundMatch[1].trim()];
|
|
145
|
-
out.missing_items = ['File must be created'];
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return out;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ─── Improvement 3: bare_minimum section ─────────────────────────────────────
|
|
152
|
-
//
|
|
153
|
-
// Derives the ordered subset of tasks required for the first native build to run.
|
|
154
|
-
// = All Tier 1 gaps + Tier 2 gaps, sorted by depends_on (topological order).
|
|
155
|
-
// Blocked tasks are included only if their blocker is also in the set.
|
|
156
|
-
|
|
157
|
-
const TIER_1_IDS = ['T1_CONFIG','T2_ROUTER_DEP','T3_ROUTES_FILE','T4_DATA_FETCHING',
|
|
158
|
-
'T5_ROUTER_DATA_PROVIDER','T6_APP_SHELL','T7_SERVER_FILES','T8_CLIENT_ENTRY'];
|
|
159
|
-
const TIER_2_IDS = ['T9_WEBVIEW_ANDROID','T10_WEBVIEW_IOS','T11_ACCESS_CONTROL',
|
|
160
|
-
'T12_SPLASH_SCREEN','T13_ANDROID_ICONS','T14_IOS_ICONS','T15_OFFLINE_HTML'];
|
|
161
|
-
|
|
162
|
-
function buildBareMinimum(steps) {
|
|
163
|
-
const bareIds = new Set([...TIER_1_IDS, ...TIER_2_IDS]);
|
|
164
|
-
const bareSteps = steps.filter(s => bareIds.has(s.id) && s.status !== 'done');
|
|
165
|
-
|
|
166
|
-
// Topological sort by depends_on
|
|
167
|
-
const sorted = [];
|
|
168
|
-
const visited = new Set();
|
|
169
|
-
|
|
170
|
-
function visit(step) {
|
|
171
|
-
if (visited.has(step.id)) return;
|
|
172
|
-
visited.add(step.id);
|
|
173
|
-
for (const depId of (step.depends_on || [])) {
|
|
174
|
-
const depStep = bareSteps.find(s => s.id === depId);
|
|
175
|
-
if (depStep) visit(depStep);
|
|
176
|
-
}
|
|
177
|
-
sorted.push(step);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
for (const s of bareSteps) visit(s);
|
|
181
|
-
|
|
182
|
-
return sorted.map((s, i) => ({
|
|
183
|
-
order: i + 1,
|
|
184
|
-
id: s.id,
|
|
185
|
-
tier: s.tier,
|
|
186
|
-
title: s.title,
|
|
187
|
-
status: s.status,
|
|
188
|
-
files_to_touch: s.files_to_touch || [],
|
|
189
|
-
missing_items: s.missing_items || [],
|
|
190
|
-
}));
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ─── Main: build conversion steps with all 3 improvements ────────────────────
|
|
194
|
-
|
|
195
|
-
// Run live conversion detection and build a personalised step list from results.
|
|
196
|
-
// Completed tasks → pre-marked done. Gaps → pending with native_risk + fix_guide.
|
|
197
|
-
// needs_review → resolved inline to gap or completed. blocked → blocked status.
|
|
198
|
-
// Adds files_to_touch + missing_items per step. Builds bare_minimum block.
|
|
199
|
-
function getConversionStepsForGoal(goal, projectRoot) {
|
|
200
|
-
if (!/convert|universal|native|migration|migrate/i.test(goal)) return null;
|
|
201
|
-
|
|
202
|
-
let status;
|
|
203
|
-
try {
|
|
204
|
-
status = conversion.handle_get_conversion_status({});
|
|
205
|
-
} catch {
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
if (!status || status.error) return null;
|
|
209
|
-
|
|
210
|
-
const steps = [];
|
|
211
|
-
let i = 0;
|
|
212
|
-
|
|
213
|
-
// Completed tasks — pre-marked done
|
|
214
|
-
for (const t of (status.completed || [])) {
|
|
215
|
-
steps.push({
|
|
216
|
-
index: i++,
|
|
217
|
-
id: t.id,
|
|
218
|
-
tier: t.tier,
|
|
219
|
-
title: t.title,
|
|
220
|
-
detail: `Already complete.${t.note ? ' Note: ' + t.note : ''}`,
|
|
221
|
-
guide: null,
|
|
222
|
-
status: 'done',
|
|
223
|
-
note: t.note || null,
|
|
224
|
-
updated_at: null,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Gaps — what needs fixing, ordered by tier
|
|
229
|
-
for (const t of (status.gaps || [])) {
|
|
230
|
-
const { files_to_touch, missing_items } = deriveFilesTouched(t, projectRoot);
|
|
231
|
-
steps.push({
|
|
232
|
-
index: i++,
|
|
233
|
-
id: t.id,
|
|
234
|
-
tier: t.tier,
|
|
235
|
-
title: t.title,
|
|
236
|
-
detail: t.reason,
|
|
237
|
-
native_risk: t.native_risk,
|
|
238
|
-
guide: t.fix_guide,
|
|
239
|
-
verify_hint: t.verify_hint || null,
|
|
240
|
-
depends_on: t.depends_on,
|
|
241
|
-
files_to_touch,
|
|
242
|
-
missing_items,
|
|
243
|
-
status: 'pending',
|
|
244
|
-
note: null,
|
|
245
|
-
updated_at: null,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// needs_review — resolve inline; no needs_review in final plan
|
|
250
|
-
for (const t of (status.needs_review || [])) {
|
|
251
|
-
const resolution = resolveNeedsReview(t, projectRoot);
|
|
252
|
-
if (resolution.verdict === 'completed') {
|
|
253
|
-
steps.push({
|
|
254
|
-
index: i++,
|
|
255
|
-
id: t.id,
|
|
256
|
-
tier: t.tier,
|
|
257
|
-
title: t.title,
|
|
258
|
-
detail: `Resolved as complete. ${resolution.evidence}`,
|
|
259
|
-
guide: null,
|
|
260
|
-
status: 'done',
|
|
261
|
-
note: resolution.evidence,
|
|
262
|
-
updated_at: null,
|
|
263
|
-
});
|
|
264
|
-
} else {
|
|
265
|
-
// Resolved as gap
|
|
266
|
-
const { files_to_touch, missing_items } = deriveFilesTouched(t, projectRoot);
|
|
267
|
-
steps.push({
|
|
268
|
-
index: i++,
|
|
269
|
-
id: t.id,
|
|
270
|
-
tier: t.tier,
|
|
271
|
-
title: t.title,
|
|
272
|
-
detail: resolution.evidence,
|
|
273
|
-
native_risk: t.native_risk,
|
|
274
|
-
guide: t.fix_guide,
|
|
275
|
-
verify_hint: t.verify_hint || null,
|
|
276
|
-
depends_on: t.depends_on,
|
|
277
|
-
files_to_touch,
|
|
278
|
-
missing_items,
|
|
279
|
-
status: 'pending',
|
|
280
|
-
resolved_from: 'needs_review',
|
|
281
|
-
note: null,
|
|
282
|
-
updated_at: null,
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Blocked — dependency not met
|
|
288
|
-
for (const t of (status.blocked || [])) {
|
|
289
|
-
steps.push({
|
|
290
|
-
index: i++,
|
|
291
|
-
id: t.id,
|
|
292
|
-
tier: t.tier,
|
|
293
|
-
title: t.title,
|
|
294
|
-
detail: `Blocked — depends on: ${(t.depends_on || []).join(', ')}`,
|
|
295
|
-
depends_on: t.depends_on,
|
|
296
|
-
status: 'blocked',
|
|
297
|
-
note: null,
|
|
298
|
-
updated_at: null,
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (steps.length === 0) return null;
|
|
303
|
-
|
|
304
|
-
// Re-index
|
|
305
|
-
steps.forEach((s, idx) => { s.index = idx; });
|
|
306
|
-
|
|
307
|
-
// Build bare_minimum block (Improvement 3)
|
|
308
|
-
const bare_minimum = buildBareMinimum(steps);
|
|
309
|
-
|
|
310
|
-
return { steps, scan_summary: status.summary, bare_minimum };
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Generate generic steps when no conversion tasks apply.
|
|
314
|
-
// These are scaffolded from the goal string — Claude will flesh them out.
|
|
315
|
-
function scaffoldStepsFromGoal(goal) {
|
|
316
|
-
return [
|
|
317
|
-
{ index: 0, title: 'Understand current state', detail: `Review existing code relevant to: ${goal}`, status: 'pending', note: null, updated_at: null },
|
|
318
|
-
{ index: 1, title: 'Identify gaps', detail: 'List what needs to change vs. what is already done.', status: 'pending', note: null, updated_at: null },
|
|
319
|
-
{ index: 2, title: 'Implement changes', detail: 'Make the required code/config changes.', status: 'pending', note: null, updated_at: null },
|
|
320
|
-
{ index: 3, title: 'Test on target platform', detail: 'Verify the change works. Check for regressions.', status: 'pending', note: null, updated_at: null },
|
|
321
|
-
{ index: 4, title: 'Mark complete + document', detail: 'Note any findings, edge cases, or follow-up tasks.', status: 'pending', note: null, updated_at: null },
|
|
322
|
-
];
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function getActivePlan(projectRoot) {
|
|
326
|
-
return _db.prepare(`
|
|
327
|
-
SELECT * FROM task_plans
|
|
328
|
-
WHERE project_root = ? AND status = 'active'
|
|
329
|
-
ORDER BY updated_at DESC LIMIT 1
|
|
330
|
-
`).get(projectRoot);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function parsePlan(row) {
|
|
334
|
-
return {
|
|
335
|
-
...row,
|
|
336
|
-
steps: JSON.parse(row.steps),
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function summarisePlan(plan) {
|
|
341
|
-
const steps = plan.steps;
|
|
342
|
-
const done = steps.filter(s => s.status === 'done').length;
|
|
343
|
-
const blocked = steps.filter(s => s.status === 'blocked').length;
|
|
344
|
-
const pending = steps.filter(s => s.status === 'pending').length;
|
|
345
|
-
const next = steps.find(s => s.status === 'pending') || null;
|
|
346
|
-
const current = steps.find(s => s.status === 'in_progress') || next;
|
|
347
|
-
return { total: steps.length, done, blocked, pending, current_step: current };
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// ─── MD file helpers ─────────────────────────────────────────────────────────
|
|
351
|
-
|
|
352
|
-
function getMdPath(projectRoot, slug) {
|
|
353
|
-
return path.join(projectRoot, '.mcp_tasks', `${slug}.md`);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function buildUserReviewWarnings(projectRoot) {
|
|
357
|
-
const warnings = [];
|
|
358
|
-
const configPath = path.join(projectRoot, 'config', 'config.json');
|
|
359
|
-
let config = null;
|
|
360
|
-
try { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch { return warnings; }
|
|
361
|
-
|
|
362
|
-
const wc = config.WEBVIEW_CONFIG || {};
|
|
363
|
-
|
|
364
|
-
// Android warnings
|
|
365
|
-
const a = wc.android || {};
|
|
366
|
-
if (a.sdkPath && !fs.existsSync(a.sdkPath)) {
|
|
367
|
-
warnings.push(`Android SDK path "${a.sdkPath}" does not exist on this machine. Update WEBVIEW_CONFIG.android.sdkPath.`);
|
|
368
|
-
}
|
|
369
|
-
if (!a.emulatorName) {
|
|
370
|
-
warnings.push('Android emulator name not set. Add WEBVIEW_CONFIG.android.emulatorName (run `emulator -list-avds` to find yours).');
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// iOS warnings
|
|
374
|
-
const ios = wc.ios || {};
|
|
375
|
-
if (!wc.ios) {
|
|
376
|
-
warnings.push('WEBVIEW_CONFIG.ios block is missing. iOS build will not work until added.');
|
|
377
|
-
} else {
|
|
378
|
-
if (!ios.appBundleId) {
|
|
379
|
-
warnings.push('iOS app bundle ID not set. Add WEBVIEW_CONFIG.ios.appBundleId (e.g. com.company.appname). Must match your Apple Developer provisioning profile.');
|
|
380
|
-
}
|
|
381
|
-
if (!ios.simulatorName) {
|
|
382
|
-
warnings.push('iOS simulator name not set. Add WEBVIEW_CONFIG.ios.simulatorName (run `xcrun simctl list devices` to find yours).');
|
|
383
|
-
}
|
|
384
|
-
if (!ios.appName) {
|
|
385
|
-
warnings.push('iOS app name not set. Add WEBVIEW_CONFIG.ios.appName.');
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return warnings;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function writeMdFile(projectRoot, slug, goal, steps, bareMinimum, userReviewWarnings) {
|
|
393
|
-
const dir = path.join(projectRoot, '.mcp_tasks');
|
|
394
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
395
|
-
|
|
396
|
-
const mdPath = getMdPath(projectRoot, slug);
|
|
397
|
-
const today = now().slice(0, 10);
|
|
398
|
-
|
|
399
|
-
// Steps section
|
|
400
|
-
const stepLines = steps.map(s => {
|
|
401
|
-
const check = s.status === 'done' ? 'x' : ' ';
|
|
402
|
-
const tier = s.tier ? ` [T${s.tier}]` : '';
|
|
403
|
-
const risk = s.native_risk ? ` ⚠ ${s.native_risk}` : '';
|
|
404
|
-
const verify = s.verify_hint ? `\n > ✓ ${s.verify_hint}` : '';
|
|
405
|
-
return `- [${check}]${tier} ${s.title}${risk}${verify}`;
|
|
406
|
-
}).join('\n');
|
|
407
|
-
|
|
408
|
-
// Current step
|
|
409
|
-
const currentStep = steps.find(s => s.status === 'in_progress') || steps.find(s => s.status === 'pending');
|
|
410
|
-
|
|
411
|
-
// Bare minimum section
|
|
412
|
-
const bareLines = bareMinimum && bareMinimum.length
|
|
413
|
-
? bareMinimum.map(s => `${s.order}. [${s.status === 'done' ? 'x' : ' '}] ${s.title}`).join('\n')
|
|
414
|
-
: '_All Tier 1+2 tasks complete._';
|
|
415
|
-
|
|
416
|
-
// User review warnings
|
|
417
|
-
const reviewLines = userReviewWarnings && userReviewWarnings.length
|
|
418
|
-
? userReviewWarnings.map(w => `- ⚠ ${w}`).join('\n')
|
|
419
|
-
: '_No manual review items._';
|
|
420
|
-
|
|
421
|
-
const content = `# Task: ${slug}
|
|
422
|
-
**Status:** in_progress
|
|
423
|
-
**Created:** ${today}
|
|
424
|
-
**Project:** ${path.basename(projectRoot)}
|
|
425
|
-
|
|
426
|
-
---
|
|
427
|
-
|
|
428
|
-
## Goal
|
|
429
|
-
${goal}
|
|
430
|
-
|
|
431
|
-
---
|
|
432
|
-
|
|
433
|
-
## User Review Required
|
|
434
|
-
${reviewLines}
|
|
435
|
-
|
|
436
|
-
---
|
|
437
|
-
|
|
438
|
-
## Bare Minimum (first native build)
|
|
439
|
-
${bareLines}
|
|
440
|
-
|
|
441
|
-
---
|
|
442
|
-
|
|
443
|
-
## Steps
|
|
444
|
-
${stepLines}
|
|
445
|
-
|
|
446
|
-
---
|
|
447
|
-
|
|
448
|
-
## Current Step
|
|
449
|
-
${currentStep ? currentStep.title : 'All steps complete.'}
|
|
450
|
-
|
|
451
|
-
---
|
|
452
|
-
|
|
453
|
-
## Findings
|
|
454
|
-
<!-- APPEND ONLY — never edit above this line -->
|
|
455
|
-
|
|
456
|
-
## Blockers
|
|
457
|
-
<!-- APPEND ONLY -->
|
|
458
|
-
|
|
459
|
-
## Decisions
|
|
460
|
-
<!-- APPEND ONLY -->
|
|
461
|
-
`;
|
|
462
|
-
|
|
463
|
-
fs.writeFileSync(mdPath, content, 'utf8');
|
|
464
|
-
return mdPath;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function updateMdFile(projectRoot, slug, steps) {
|
|
468
|
-
const mdPath = getMdPath(projectRoot, slug);
|
|
469
|
-
if (!fs.existsSync(mdPath)) return; // MD missing — skip silently, DB is source of truth
|
|
470
|
-
|
|
471
|
-
let content = fs.readFileSync(mdPath, 'utf8');
|
|
472
|
-
|
|
473
|
-
// Rebuild Steps section
|
|
474
|
-
const stepLines = steps.map(s => {
|
|
475
|
-
const check = s.status === 'done' ? 'x' : ' ';
|
|
476
|
-
const tier = s.tier ? ` [T${s.tier}]` : '';
|
|
477
|
-
const risk = s.native_risk ? ` ⚠ ${s.native_risk}` : '';
|
|
478
|
-
return `- [${check}]${tier} ${s.title}${risk}`;
|
|
479
|
-
}).join('\n');
|
|
480
|
-
|
|
481
|
-
content = content.replace(
|
|
482
|
-
/^## Steps\n[\s\S]*?(?=\n---)/m,
|
|
483
|
-
`## Steps\n${stepLines}`
|
|
484
|
-
);
|
|
485
|
-
|
|
486
|
-
// Update Current Step
|
|
487
|
-
const currentStep = steps.find(s => s.status === 'in_progress') || steps.find(s => s.status === 'pending');
|
|
488
|
-
content = content.replace(
|
|
489
|
-
/^## Current Step\n.*/m,
|
|
490
|
-
`## Current Step\n${currentStep ? currentStep.title : 'All steps complete.'}`
|
|
491
|
-
);
|
|
492
|
-
|
|
493
|
-
fs.writeFileSync(mdPath, content, 'utf8');
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function appendToMdFindings(projectRoot, slug, finding) {
|
|
497
|
-
const mdPath = getMdPath(projectRoot, slug);
|
|
498
|
-
if (!fs.existsSync(mdPath)) return;
|
|
499
|
-
|
|
500
|
-
const line = `\n- ${now()} | ${finding}`;
|
|
501
|
-
let content = fs.readFileSync(mdPath, 'utf8');
|
|
502
|
-
content = content.replace('## Findings\n<!-- APPEND ONLY — never edit above this line -->', `## Findings\n<!-- APPEND ONLY — never edit above this line -->${line}`);
|
|
503
|
-
fs.writeFileSync(mdPath, content, 'utf8');
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// ─── Tool: create_task_plan ──────────────────────────────────────────────────
|
|
507
|
-
|
|
508
|
-
// Commands that are verified to exist in catalyst-core projects
|
|
509
|
-
const VALID_COMMANDS = new Set([
|
|
510
|
-
'npm run build:android',
|
|
511
|
-
'npm run build:android:release',
|
|
512
|
-
'npm run build:ios',
|
|
513
|
-
'npm run buildApp:android',
|
|
514
|
-
'npm run buildApp:ios',
|
|
515
|
-
'npm run buildApp:android:release',
|
|
516
|
-
'npm run buildApp:ios:release',
|
|
517
|
-
'npm run setupEmulator:android',
|
|
518
|
-
'npm run setupEmulator:ios',
|
|
519
|
-
'npm run devBuild',
|
|
520
|
-
'npm run devServe',
|
|
521
|
-
'npm run prepare',
|
|
522
|
-
'node .catalyst/mcp/setup.js',
|
|
523
|
-
]);
|
|
524
|
-
|
|
525
|
-
function validateStepCommands(steps) {
|
|
526
|
-
const invalid = [];
|
|
527
|
-
for (const s of steps) {
|
|
528
|
-
const text = `${typeof s === 'string' ? s : (s.title || '') + ' ' + (s.detail || '')}`;
|
|
529
|
-
const cmds = text.match(/npm [\w:]+(?:\s[\w:]+)?|npx [\w@/-]+|node [\w./]+/g) || [];
|
|
530
|
-
for (const cmd of cmds) {
|
|
531
|
-
if (!VALID_COMMANDS.has(cmd)) {
|
|
532
|
-
invalid.push(cmd);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
return invalid;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function handle_create_task_plan({ goal, steps: customSteps } = {}) {
|
|
540
|
-
if (!goal) return { error: 'goal is required.' };
|
|
541
|
-
|
|
542
|
-
const catalystRoot = findCatalystRoot();
|
|
543
|
-
if (!catalystRoot) return { error: 'No catalyst-core project found.' };
|
|
544
|
-
|
|
545
|
-
const projectRoot = catalystRoot.dir;
|
|
546
|
-
|
|
547
|
-
// Abandon any existing active plan for this project
|
|
548
|
-
const existing = getActivePlan(projectRoot);
|
|
549
|
-
if (existing) {
|
|
550
|
-
_db.prepare(`UPDATE task_plans SET status='abandoned', updated_at=? WHERE id=?`)
|
|
551
|
-
.run(now(), existing.id);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Build steps
|
|
555
|
-
let steps;
|
|
556
|
-
let scan_summary = null;
|
|
557
|
-
let bare_minimum = null;
|
|
558
|
-
|
|
559
|
-
if (customSteps && Array.isArray(customSteps) && customSteps.length) {
|
|
560
|
-
// Validate any commands in custom steps against known catalyst commands
|
|
561
|
-
const invalidCmds = validateStepCommands(customSteps);
|
|
562
|
-
if (invalidCmds.length > 0) {
|
|
563
|
-
return {
|
|
564
|
-
error: 'invalid_commands_in_steps',
|
|
565
|
-
message: `Steps contain commands that do not exist in catalyst-core projects: ${invalidCmds.join(', ')}. Do not invent commands. Valid catalyst commands are: ${[...VALID_COMMANDS].join(', ')}. For conversion/migration goals, omit steps entirely — auto-generation runs live file detection and builds accurate steps.`,
|
|
566
|
-
invalid_commands: invalidCmds,
|
|
567
|
-
valid_commands: [...VALID_COMMANDS],
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
steps = customSteps.map((s, i) => ({
|
|
571
|
-
index: i,
|
|
572
|
-
title: typeof s === 'string' ? s : s.title,
|
|
573
|
-
detail: typeof s === 'string' ? '' : (s.detail || ''),
|
|
574
|
-
status: 'pending',
|
|
575
|
-
note: null,
|
|
576
|
-
updated_at: null,
|
|
577
|
-
}));
|
|
578
|
-
} else {
|
|
579
|
-
const result = getConversionStepsForGoal(goal, projectRoot);
|
|
580
|
-
if (result) {
|
|
581
|
-
steps = result.steps;
|
|
582
|
-
scan_summary = result.scan_summary;
|
|
583
|
-
bare_minimum = result.bare_minimum;
|
|
584
|
-
} else {
|
|
585
|
-
steps = scaffoldStepsFromGoal(goal);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// Mark first non-done step in_progress
|
|
590
|
-
const firstPending = steps.find(s => s.status === 'pending');
|
|
591
|
-
if (firstPending) firstPending.status = 'in_progress';
|
|
592
|
-
|
|
593
|
-
const slug = slugify(goal);
|
|
594
|
-
const uniqueSlug = `${slug}-${Date.now()}`;
|
|
595
|
-
|
|
596
|
-
_db.prepare(`
|
|
597
|
-
INSERT INTO task_plans (slug, goal, project_root, status, steps, created_at, updated_at)
|
|
598
|
-
VALUES (?, ?, ?, 'active', ?, ?, ?)
|
|
599
|
-
`).run(uniqueSlug, goal, projectRoot, JSON.stringify(steps), now(), now());
|
|
600
|
-
|
|
601
|
-
// Write .mcp_tasks/<slug>.md
|
|
602
|
-
const userReviewWarnings = buildUserReviewWarnings(projectRoot);
|
|
603
|
-
const mdPath = writeMdFile(projectRoot, uniqueSlug, goal, steps, bare_minimum, userReviewWarnings);
|
|
604
|
-
|
|
605
|
-
const summary = summarisePlan({ steps });
|
|
606
|
-
const pendingSteps = steps.filter(s => s.status === 'pending' || s.status === 'in_progress');
|
|
607
|
-
const resolvedFromReview = steps.filter(s => s.resolved_from === 'needs_review' && s.status !== 'done').length;
|
|
608
|
-
|
|
609
|
-
return {
|
|
610
|
-
created: true,
|
|
611
|
-
slug: uniqueSlug,
|
|
612
|
-
goal,
|
|
613
|
-
project_root: projectRoot,
|
|
614
|
-
task_file: mdPath,
|
|
615
|
-
scan_summary,
|
|
616
|
-
total_steps: steps.length,
|
|
617
|
-
done_already: steps.filter(s => s.status === 'done').length,
|
|
618
|
-
gaps: pendingSteps.length,
|
|
619
|
-
resolved_from_needs_review: resolvedFromReview || undefined,
|
|
620
|
-
blocked: steps.filter(s => s.status === 'blocked').length,
|
|
621
|
-
current_step: summary.current_step,
|
|
622
|
-
user_review: userReviewWarnings.length ? userReviewWarnings : undefined,
|
|
623
|
-
bare_minimum,
|
|
624
|
-
steps,
|
|
625
|
-
tip: `Task file written to ${mdPath}. Call get_active_task to resume. Call update_task_step to mark progress.`,
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// ─── Tool: update_task_step ──────────────────────────────────────────────────
|
|
630
|
-
|
|
631
|
-
function handle_update_task_step({ step_index, status, note, plan_slug } = {}) {
|
|
632
|
-
const catalystRoot = findCatalystRoot();
|
|
633
|
-
if (!catalystRoot) return { error: 'No catalyst-core project found.' };
|
|
634
|
-
|
|
635
|
-
const projectRoot = catalystRoot.dir;
|
|
636
|
-
|
|
637
|
-
// Resolve plan
|
|
638
|
-
let row = plan_slug
|
|
639
|
-
? _db.prepare(`SELECT * FROM task_plans WHERE slug=?`).get(plan_slug)
|
|
640
|
-
: getActivePlan(projectRoot);
|
|
641
|
-
|
|
642
|
-
if (!row) return { error: 'No active task plan found. Call create_task_plan first.' };
|
|
643
|
-
|
|
644
|
-
const plan = parsePlan(row);
|
|
645
|
-
const steps = plan.steps;
|
|
646
|
-
|
|
647
|
-
if (step_index == null || step_index < 0 || step_index >= steps.length) {
|
|
648
|
-
return { error: `step_index out of range. Plan has ${steps.length} steps (0-based).` };
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const validStatuses = ['done', 'blocked', 'skipped', 'in_progress', 'pending'];
|
|
652
|
-
const newStatus = status || 'done';
|
|
653
|
-
if (!validStatuses.includes(newStatus)) {
|
|
654
|
-
return { error: `Invalid status "${newStatus}". Use: ${validStatuses.join(' | ')}` };
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// Update the step
|
|
658
|
-
steps[step_index].status = newStatus;
|
|
659
|
-
steps[step_index].note = note || steps[step_index].note;
|
|
660
|
-
steps[step_index].updated_at = now();
|
|
661
|
-
|
|
662
|
-
// Auto-advance: if marking done, set next pending step to in_progress
|
|
663
|
-
let next_step = null;
|
|
664
|
-
if (newStatus === 'done' || newStatus === 'skipped') {
|
|
665
|
-
const nextPending = steps.find(s => s.index > step_index && s.status === 'pending');
|
|
666
|
-
if (nextPending) {
|
|
667
|
-
nextPending.status = 'in_progress';
|
|
668
|
-
nextPending.updated_at = now();
|
|
669
|
-
next_step = nextPending;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// Check if all steps are terminal
|
|
674
|
-
const allDone = steps.every(s => ['done', 'skipped', 'blocked'].includes(s.status));
|
|
675
|
-
const planStatus = allDone ? 'completed' : 'active';
|
|
676
|
-
|
|
677
|
-
_db.prepare(`
|
|
678
|
-
UPDATE task_plans SET steps=?, status=?, updated_at=? WHERE id=?
|
|
679
|
-
`).run(JSON.stringify(steps), planStatus, now(), row.id);
|
|
680
|
-
|
|
681
|
-
// Sync MD file
|
|
682
|
-
updateMdFile(plan.project_root, plan.slug, steps);
|
|
683
|
-
if (note) appendToMdFindings(plan.project_root, plan.slug, note);
|
|
684
|
-
|
|
685
|
-
const summary = summarisePlan({ steps });
|
|
686
|
-
|
|
687
|
-
const result = {
|
|
688
|
-
updated: true,
|
|
689
|
-
step_index,
|
|
690
|
-
new_status: newStatus,
|
|
691
|
-
note: note || null,
|
|
692
|
-
plan_status: planStatus,
|
|
693
|
-
progress: `${summary.done}/${summary.total} done`,
|
|
694
|
-
next_step,
|
|
695
|
-
all_steps: steps,
|
|
696
|
-
};
|
|
697
|
-
|
|
698
|
-
if (allDone) {
|
|
699
|
-
result.next_tool_call = {
|
|
700
|
-
tool: 'close_task_plan',
|
|
701
|
-
reason: `All ${summary.total} steps complete. You MUST call close_task_plan now. Ask the user: "Task complete — should I delete the task file at .mcp_tasks/${plan.slug}.md? (yes/no)" then call close_task_plan with delete_file:true or delete_file:false based on their answer.`,
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
return result;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// ─── Tool: get_active_task ───────────────────────────────────────────────────
|
|
709
|
-
|
|
710
|
-
function handle_get_active_task({ include_all_steps } = {}) {
|
|
711
|
-
const catalystRoot = findCatalystRoot();
|
|
712
|
-
if (!catalystRoot) return { error: 'No catalyst-core project found.' };
|
|
713
|
-
|
|
714
|
-
const projectRoot = catalystRoot.dir;
|
|
715
|
-
const row = getActivePlan(projectRoot);
|
|
716
|
-
|
|
717
|
-
if (!row) {
|
|
718
|
-
// Also check if there's a recently completed plan
|
|
719
|
-
const last = _db.prepare(`
|
|
720
|
-
SELECT * FROM task_plans WHERE project_root=?
|
|
721
|
-
ORDER BY updated_at DESC LIMIT 1
|
|
722
|
-
`).get(projectRoot);
|
|
723
|
-
|
|
724
|
-
return {
|
|
725
|
-
active_plan: null,
|
|
726
|
-
last_plan: last ? {
|
|
727
|
-
slug: last.slug,
|
|
728
|
-
goal: last.goal,
|
|
729
|
-
status: last.status,
|
|
730
|
-
updated_at: last.updated_at,
|
|
731
|
-
} : null,
|
|
732
|
-
message: last
|
|
733
|
-
? `No active plan. Last plan "${last.goal}" is ${last.status}. Call create_task_plan to start a new one.`
|
|
734
|
-
: 'No task plans found for this project. Call create_task_plan to start.',
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
const plan = parsePlan(row);
|
|
739
|
-
const summary = summarisePlan(plan);
|
|
740
|
-
|
|
741
|
-
// By default only show non-done steps to keep response tight
|
|
742
|
-
const visibleSteps = include_all_steps
|
|
743
|
-
? plan.steps
|
|
744
|
-
: plan.steps.filter(s => s.status !== 'done' && s.status !== 'skipped');
|
|
745
|
-
|
|
746
|
-
return {
|
|
747
|
-
active_plan: true,
|
|
748
|
-
slug: plan.slug,
|
|
749
|
-
goal: plan.goal,
|
|
750
|
-
project_root: plan.project_root,
|
|
751
|
-
task_file: getMdPath(plan.project_root, plan.slug),
|
|
752
|
-
created_at: plan.created_at,
|
|
753
|
-
updated_at: plan.updated_at,
|
|
754
|
-
progress: `${summary.done}/${summary.total} done`,
|
|
755
|
-
blocked: summary.blocked > 0 ? summary.blocked : undefined,
|
|
756
|
-
current_step: summary.current_step,
|
|
757
|
-
pending_steps: visibleSteps,
|
|
758
|
-
tip: include_all_steps ? undefined : 'Pass include_all_steps:true to see completed steps too.',
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// ─── Tool: close_task_plan ───────────────────────────────────────────────────
|
|
763
|
-
|
|
764
|
-
function handle_close_task_plan({ delete_file = false, plan_slug } = {}) {
|
|
765
|
-
const catalystRoot = findCatalystRoot();
|
|
766
|
-
if (!catalystRoot) return { error: 'No catalyst-core project found.' };
|
|
767
|
-
|
|
768
|
-
const projectRoot = catalystRoot.dir;
|
|
769
|
-
|
|
770
|
-
const row = plan_slug
|
|
771
|
-
? _db.prepare(`SELECT * FROM task_plans WHERE slug=?`).get(plan_slug)
|
|
772
|
-
: getActivePlan(projectRoot) || _db.prepare(`
|
|
773
|
-
SELECT * FROM task_plans WHERE project_root=? AND status='completed'
|
|
774
|
-
ORDER BY updated_at DESC LIMIT 1
|
|
775
|
-
`).get(projectRoot);
|
|
776
|
-
|
|
777
|
-
if (!row) return { error: 'No active or recently completed task plan found.' };
|
|
778
|
-
|
|
779
|
-
const plan = parsePlan(row);
|
|
780
|
-
const steps = plan.steps;
|
|
781
|
-
const incomplete = steps.filter(s => !['done', 'skipped', 'blocked'].includes(s.status));
|
|
782
|
-
|
|
783
|
-
if (incomplete.length > 0) {
|
|
784
|
-
return {
|
|
785
|
-
error: 'Plan has incomplete steps. Mark all steps done before closing.',
|
|
786
|
-
incomplete: incomplete.map(s => ({ index: s.index, title: s.title, status: s.status })),
|
|
787
|
-
tip: 'Use update_task_step to mark remaining steps, or pass status:"skipped" to skip them.',
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// Mark DB record as closed
|
|
792
|
-
_db.prepare(`UPDATE task_plans SET status='closed', updated_at=? WHERE id=?`)
|
|
793
|
-
.run(now(), row.id);
|
|
794
|
-
|
|
795
|
-
const mdPath = getMdPath(projectRoot, plan.slug);
|
|
796
|
-
let file_deleted = false;
|
|
797
|
-
|
|
798
|
-
if (delete_file) {
|
|
799
|
-
if (fs.existsSync(mdPath)) {
|
|
800
|
-
fs.unlinkSync(mdPath);
|
|
801
|
-
file_deleted = true;
|
|
802
|
-
// Clean up .mcp_tasks/ dir if empty
|
|
803
|
-
const dir = path.dirname(mdPath);
|
|
804
|
-
try {
|
|
805
|
-
if (fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
|
|
806
|
-
} catch { /* ignore */ }
|
|
807
|
-
}
|
|
808
|
-
} else {
|
|
809
|
-
// Update MD status to done
|
|
810
|
-
if (fs.existsSync(mdPath)) {
|
|
811
|
-
let content = fs.readFileSync(mdPath, 'utf8');
|
|
812
|
-
content = content.replace('**Status:** in_progress', `**Status:** done`);
|
|
813
|
-
content = content.replace(
|
|
814
|
-
'## Findings\n<!-- APPEND ONLY — never edit above this line -->',
|
|
815
|
-
`## Findings\n<!-- APPEND ONLY — never edit above this line -->\n- ${now()} | Task closed. All steps complete.`
|
|
816
|
-
);
|
|
817
|
-
fs.writeFileSync(mdPath, content, 'utf8');
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
const done = steps.filter(s => s.status === 'done').length;
|
|
822
|
-
const skipped = steps.filter(s => s.status === 'skipped').length;
|
|
823
|
-
const blocked = steps.filter(s => s.status === 'blocked').length;
|
|
824
|
-
|
|
825
|
-
return {
|
|
826
|
-
closed: true,
|
|
827
|
-
slug: plan.slug,
|
|
828
|
-
goal: plan.goal,
|
|
829
|
-
summary: `${done} done, ${skipped} skipped, ${blocked} blocked of ${steps.length} total.`,
|
|
830
|
-
file_deleted,
|
|
831
|
-
task_file: file_deleted ? null : mdPath,
|
|
832
|
-
message: file_deleted
|
|
833
|
-
? `Plan closed and task file deleted.`
|
|
834
|
-
: `Plan closed. Task file kept at ${mdPath}. Delete .mcp_tasks/ manually when done reviewing.`,
|
|
835
|
-
};
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
module.exports = {
|
|
839
|
-
init,
|
|
840
|
-
handle_create_task_plan,
|
|
841
|
-
handle_update_task_step,
|
|
842
|
-
handle_get_active_task,
|
|
843
|
-
handle_close_task_plan,
|
|
844
|
-
};
|