@synergenius/flow-weaver-pack-weaver 0.9.151 → 0.9.153
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/dist/ai-chat-provider.js +4 -4
- package/dist/ai-chat-provider.js.map +1 -1
- package/dist/bot/ai-client.d.ts +30 -0
- package/dist/bot/ai-client.d.ts.map +1 -1
- package/dist/bot/ai-client.js +37 -0
- package/dist/bot/ai-client.js.map +1 -1
- package/dist/bot/behavior-defaults.d.ts.map +1 -1
- package/dist/bot/behavior-defaults.js +7 -2
- package/dist/bot/behavior-defaults.js.map +1 -1
- package/dist/bot/capability-registry.d.ts.map +1 -1
- package/dist/bot/capability-registry.js +48 -30
- package/dist/bot/capability-registry.js.map +1 -1
- package/dist/bot/file-validator.d.ts +7 -0
- package/dist/bot/file-validator.d.ts.map +1 -1
- package/dist/bot/file-validator.js +76 -0
- package/dist/bot/file-validator.js.map +1 -1
- package/dist/bot/instance-manager.d.ts +22 -7
- package/dist/bot/instance-manager.d.ts.map +1 -1
- package/dist/bot/instance-manager.js +69 -7
- package/dist/bot/instance-manager.js.map +1 -1
- package/dist/bot/orchestrator.d.ts +11 -9
- package/dist/bot/orchestrator.d.ts.map +1 -1
- package/dist/bot/orchestrator.js +56 -107
- package/dist/bot/orchestrator.js.map +1 -1
- package/dist/bot/runner.d.ts +29 -0
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +114 -73
- package/dist/bot/runner.js.map +1 -1
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +106 -25
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/swarm-controller.d.ts +7 -6
- package/dist/bot/swarm-controller.d.ts.map +1 -1
- package/dist/bot/swarm-controller.js +64 -74
- package/dist/bot/swarm-controller.js.map +1 -1
- package/dist/bot/task-types.d.ts +1 -0
- package/dist/bot/task-types.d.ts.map +1 -1
- package/dist/bot/weaver-tools.d.ts +1 -1
- package/dist/bot/weaver-tools.d.ts.map +1 -1
- package/dist/bot/weaver-tools.js +6 -1
- package/dist/bot/weaver-tools.js.map +1 -1
- package/dist/node-types/agent-execute.js +2 -2
- package/dist/node-types/agent-execute.js.map +1 -1
- package/dist/node-types/bot-report.d.ts.map +1 -1
- package/dist/node-types/bot-report.js +5 -2
- package/dist/node-types/bot-report.js.map +1 -1
- package/dist/node-types/build-context.js +2 -1
- package/dist/node-types/build-context.js.map +1 -1
- package/dist/node-types/exec-validate-retry.d.ts +3 -3
- package/dist/node-types/exec-validate-retry.d.ts.map +1 -1
- package/dist/node-types/exec-validate-retry.js +13 -184
- package/dist/node-types/exec-validate-retry.js.map +1 -1
- package/dist/node-types/load-config.d.ts +1 -0
- package/dist/node-types/load-config.d.ts.map +1 -1
- package/dist/node-types/load-config.js +1 -0
- package/dist/node-types/load-config.js.map +1 -1
- package/dist/node-types/plan-task.d.ts +7 -5
- package/dist/node-types/plan-task.d.ts.map +1 -1
- package/dist/node-types/plan-task.js +282 -83
- package/dist/node-types/plan-task.js.map +1 -1
- package/dist/ui/bot-panel.js +1 -1
- package/dist/ui/capability-editor.js +48 -30
- package/dist/ui/profile-editor.js +46 -28
- package/dist/ui/swarm-dashboard.js +71 -33
- package/dist/ui/task-detail-view.js +22 -2
- package/dist/workflows/weaver-bot.d.ts +2 -2
- package/dist/workflows/weaver-bot.d.ts.map +1 -1
- package/dist/workflows/weaver-bot.js +5 -4
- package/dist/workflows/weaver-bot.js.map +1 -1
- package/flowweaver.manifest.json +1 -1
- package/package.json +1 -1
- package/src/ai-chat-provider.ts +4 -4
- package/src/bot/ai-client.ts +65 -0
- package/src/bot/behavior-defaults.ts +5 -2
- package/src/bot/capability-registry.ts +48 -30
- package/src/bot/file-validator.ts +97 -0
- package/src/bot/instance-manager.ts +77 -7
- package/src/bot/orchestrator.ts +63 -126
- package/src/bot/runner.ts +124 -70
- package/src/bot/step-executor.ts +115 -25
- package/src/bot/swarm-controller.ts +65 -76
- package/src/bot/task-types.ts +1 -0
- package/src/bot/weaver-tools.ts +7 -1
- package/src/node-types/agent-execute.ts +2 -2
- package/src/node-types/bot-report.ts +5 -2
- package/src/node-types/build-context.ts +2 -1
- package/src/node-types/exec-validate-retry.ts +14 -203
- package/src/node-types/load-config.ts +1 -0
- package/src/node-types/plan-task.ts +313 -88
- package/src/ui/bot-panel.tsx +1 -1
- package/src/ui/swarm-dashboard.tsx +3 -3
- package/src/ui/task-detail-view.tsx +25 -2
- package/src/workflows/weaver-bot.ts +5 -4
package/src/bot/runner.ts
CHANGED
|
@@ -84,7 +84,7 @@ function parseConfigFile(configPath: string): WeaverConfig {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function buildSummary(result: unknown): string {
|
|
87
|
+
export function buildSummary(result: unknown): string {
|
|
88
88
|
if (!result || typeof result !== 'object') return String(result);
|
|
89
89
|
|
|
90
90
|
const r = result as Record<string, unknown>;
|
|
@@ -101,6 +101,120 @@ function buildSummary(result: unknown): string {
|
|
|
101
101
|
return parts.length > 0 ? parts.join(', ') : 'completed';
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Build a markdown report from the workflow result's ctx field.
|
|
106
|
+
* Extracted for testability — same logic used inside runWorkflow.
|
|
107
|
+
*/
|
|
108
|
+
export function buildReport(
|
|
109
|
+
result: Record<string, unknown> | null,
|
|
110
|
+
success: boolean,
|
|
111
|
+
stepLog?: import('./types.js').StepLogEntry[],
|
|
112
|
+
): string | undefined {
|
|
113
|
+
try {
|
|
114
|
+
const ctxStr = result?.ctx as string | undefined;
|
|
115
|
+
if (!ctxStr) return undefined;
|
|
116
|
+
const ctx = JSON.parse(ctxStr);
|
|
117
|
+
const md: string[] = [];
|
|
118
|
+
md.push(`## ${success ? 'Task Completed' : 'Task Failed'}`);
|
|
119
|
+
md.push('');
|
|
120
|
+
|
|
121
|
+
// Steps
|
|
122
|
+
if (stepLog && stepLog.length > 0) {
|
|
123
|
+
md.push('### Steps');
|
|
124
|
+
md.push('');
|
|
125
|
+
for (const s of stepLog) {
|
|
126
|
+
const icon = s.status === 'ok' ? '**ok**' : s.status === 'error' ? '**error**' : '**blocked**';
|
|
127
|
+
md.push(`- ${s.step} (${icon})${s.detail ? `: ${s.detail}` : ''}`);
|
|
128
|
+
}
|
|
129
|
+
md.push('');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Files
|
|
133
|
+
const files: string[] = ctx.filesModified ? JSON.parse(ctx.filesModified) : [];
|
|
134
|
+
if (files.length > 0) {
|
|
135
|
+
md.push('### Files Modified');
|
|
136
|
+
md.push('');
|
|
137
|
+
for (const f of files) md.push(`- \`${f}\``);
|
|
138
|
+
md.push('');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Review
|
|
142
|
+
if (ctx.reviewJson) {
|
|
143
|
+
const review = JSON.parse(ctx.reviewJson) as Record<string, string>;
|
|
144
|
+
if (review.intent || review.execution || review.result || review.completeness) {
|
|
145
|
+
md.push('### Review');
|
|
146
|
+
md.push('');
|
|
147
|
+
for (const key of ['intent', 'execution', 'result', 'completeness']) {
|
|
148
|
+
if (review[key]) md.push(`- **${key}:** ${review[key]}`);
|
|
149
|
+
}
|
|
150
|
+
if (review.reason) md.push(`\n${review.reason}`);
|
|
151
|
+
md.push('');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return md.length > 2 ? md.join('\n') : undefined;
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Extract stepLog and plan from the result's ctx field.
|
|
163
|
+
* Extracted for testability — same logic used inside runWorkflow.
|
|
164
|
+
*/
|
|
165
|
+
export function extractCtxData(result: Record<string, unknown> | null): {
|
|
166
|
+
stepLog?: import('./types.js').StepLogEntry[];
|
|
167
|
+
plan?: { summary: string; steps: Array<{ id: string; operation: string; description: string; args?: Record<string, unknown> }> };
|
|
168
|
+
} {
|
|
169
|
+
try {
|
|
170
|
+
const ctxStr = result?.ctx as string | undefined;
|
|
171
|
+
if (!ctxStr) return {};
|
|
172
|
+
const ctx = JSON.parse(ctxStr);
|
|
173
|
+
let stepLog: import('./types.js').StepLogEntry[] | undefined;
|
|
174
|
+
let plan: { summary: string; steps: Array<{ id: string; operation: string; description: string; args?: Record<string, unknown> }> } | undefined;
|
|
175
|
+
|
|
176
|
+
if (ctx.stepLogJson) stepLog = JSON.parse(ctx.stepLogJson);
|
|
177
|
+
if (ctx.planJson) {
|
|
178
|
+
const parsed = JSON.parse(ctx.planJson);
|
|
179
|
+
if (parsed?.steps) {
|
|
180
|
+
plan = {
|
|
181
|
+
summary: parsed.summary ?? '',
|
|
182
|
+
steps: (parsed.steps as Array<Record<string, unknown>>).map((s) => ({
|
|
183
|
+
id: String(s.id ?? ''),
|
|
184
|
+
operation: String(s.operation ?? ''),
|
|
185
|
+
description: String(s.description ?? ''),
|
|
186
|
+
...(s.args ? { args: s.args as Record<string, unknown> } : {}),
|
|
187
|
+
})),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { stepLog, plan };
|
|
192
|
+
} catch {
|
|
193
|
+
return {};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Fallback: extract report from trace outputs (bot-report node's "Markdown report" port).
|
|
199
|
+
*/
|
|
200
|
+
export function extractReportFromTrace(
|
|
201
|
+
trace: Array<{ type: string; outputs?: unknown[] }>,
|
|
202
|
+
): string | undefined {
|
|
203
|
+
for (const te of trace) {
|
|
204
|
+
if (te.type === 'node-complete' && te.outputs) {
|
|
205
|
+
for (const o of te.outputs) {
|
|
206
|
+
if (typeof o === 'object' && o && 'portLabel' in o) {
|
|
207
|
+
const po = o as { portLabel: string; value: unknown };
|
|
208
|
+
if (po.portLabel === 'Markdown report' && typeof po.value === 'string' && po.value.length > 0) {
|
|
209
|
+
return po.value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
104
218
|
export async function runWorkflow(
|
|
105
219
|
filePath: string,
|
|
106
220
|
options?: {
|
|
@@ -272,7 +386,7 @@ export async function runWorkflow(
|
|
|
272
386
|
}
|
|
273
387
|
}
|
|
274
388
|
}
|
|
275
|
-
for (const inst of parseResult?.workflow?.instances ?? []) {
|
|
389
|
+
for (const inst of (parseResult?.workflows?.[0]?.instances ?? parseResult?.workflow?.instances ?? [])) {
|
|
276
390
|
const label = inst.config?.label ?? nodeLabels.get(inst.nodeType);
|
|
277
391
|
if (label) nodeLabels.set(inst.id, label);
|
|
278
392
|
// Inherit visual meta from node type to instance
|
|
@@ -415,77 +529,17 @@ export async function runWorkflow(
|
|
|
415
529
|
}
|
|
416
530
|
|
|
417
531
|
// Extract stepLog and plan from WeaverContext if available
|
|
418
|
-
|
|
419
|
-
let plan: RunRecord['plan'] | undefined;
|
|
420
|
-
try {
|
|
421
|
-
const ctxStr = result?.ctx as string | undefined;
|
|
422
|
-
if (ctxStr) {
|
|
423
|
-
const ctx = JSON.parse(ctxStr);
|
|
424
|
-
if (ctx.stepLogJson) stepLog = JSON.parse(ctx.stepLogJson);
|
|
425
|
-
if (ctx.planJson) {
|
|
426
|
-
const parsed = JSON.parse(ctx.planJson);
|
|
427
|
-
if (parsed?.steps) {
|
|
428
|
-
plan = {
|
|
429
|
-
summary: parsed.summary ?? '',
|
|
430
|
-
steps: (parsed.steps as Array<Record<string, unknown>>).map((s) => ({
|
|
431
|
-
id: String(s.id ?? ''),
|
|
432
|
-
operation: String(s.operation ?? ''),
|
|
433
|
-
description: String(s.description ?? ''),
|
|
434
|
-
...(s.args ? { args: s.args as Record<string, unknown> } : {}),
|
|
435
|
-
})),
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
} catch { /* extraction is best-effort */ }
|
|
532
|
+
const { stepLog, plan } = extractCtxData(result);
|
|
441
533
|
|
|
442
534
|
// Build markdown report from the extracted context data
|
|
443
|
-
let report
|
|
444
|
-
try {
|
|
445
|
-
const ctxStr = result?.ctx as string | undefined;
|
|
446
|
-
if (ctxStr) {
|
|
447
|
-
const ctx = JSON.parse(ctxStr);
|
|
448
|
-
const md: string[] = [];
|
|
449
|
-
md.push(`## ${success ? 'Task Completed' : 'Task Failed'}`);
|
|
450
|
-
md.push('');
|
|
451
|
-
|
|
452
|
-
// Steps
|
|
453
|
-
if (stepLog && stepLog.length > 0) {
|
|
454
|
-
md.push('### Steps');
|
|
455
|
-
md.push('');
|
|
456
|
-
for (const s of stepLog) {
|
|
457
|
-
const icon = s.status === 'ok' ? '**ok**' : s.status === 'error' ? '**error**' : '**blocked**';
|
|
458
|
-
md.push(`- ${s.step} (${icon})${s.detail ? `: ${s.detail}` : ''}`);
|
|
459
|
-
}
|
|
460
|
-
md.push('');
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Files
|
|
464
|
-
const files: string[] = ctx.filesModified ? JSON.parse(ctx.filesModified) : [];
|
|
465
|
-
if (files.length > 0) {
|
|
466
|
-
md.push('### Files Modified');
|
|
467
|
-
md.push('');
|
|
468
|
-
for (const f of files) md.push(`- \`${f}\``);
|
|
469
|
-
md.push('');
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Review
|
|
473
|
-
if (ctx.reviewJson) {
|
|
474
|
-
const review = JSON.parse(ctx.reviewJson) as Record<string, string>;
|
|
475
|
-
if (review.intent || review.execution || review.result || review.completeness) {
|
|
476
|
-
md.push('### Review');
|
|
477
|
-
md.push('');
|
|
478
|
-
for (const key of ['intent', 'execution', 'result', 'completeness']) {
|
|
479
|
-
if (review[key]) md.push(`- **${key}:** ${review[key]}`);
|
|
480
|
-
}
|
|
481
|
-
if (review.reason) md.push(`\n${review.reason}`);
|
|
482
|
-
md.push('');
|
|
483
|
-
}
|
|
484
|
-
}
|
|
535
|
+
let report = buildReport(result, success, stepLog);
|
|
485
536
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
537
|
+
// Fallback: extract report from the bot-report node's trace output.
|
|
538
|
+
// The FW compiled workflow doesn't wire bot-report's report port to END,
|
|
539
|
+
// so result.report is undefined. But the trace captured all node outputs.
|
|
540
|
+
if (!report) {
|
|
541
|
+
report = extractReportFromTrace(collectedTrace);
|
|
542
|
+
}
|
|
489
543
|
|
|
490
544
|
await notifier({
|
|
491
545
|
type: 'workflow-complete',
|
package/src/bot/step-executor.ts
CHANGED
|
@@ -189,18 +189,24 @@ export async function executeStep(
|
|
|
189
189
|
}
|
|
190
190
|
assertSafePath(file, projectDir);
|
|
191
191
|
const filePath = path.resolve(projectDir, file);
|
|
192
|
-
|
|
192
|
+
// Use the relative path for reporting — never leak absolute server paths
|
|
193
|
+
const relPath = path.relative(projectDir, filePath);
|
|
194
|
+
const rawContent = args.content ?? args.body ?? '';
|
|
195
|
+
if (typeof rawContent !== 'string') {
|
|
196
|
+
return { blocked: true, blockReason: `${step.operation}: "content" must be a string, got ${typeof rawContent}.` };
|
|
197
|
+
}
|
|
198
|
+
const content = rawContent;
|
|
193
199
|
|
|
194
200
|
const guard = checkWriteSafety(filePath, content);
|
|
195
201
|
if (!guard.allowed) {
|
|
196
|
-
return { file:
|
|
202
|
+
return { file: relPath, blocked: true, blockReason: guard.reason };
|
|
197
203
|
}
|
|
198
204
|
|
|
199
205
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
200
206
|
const existed = fs.existsSync(filePath);
|
|
201
207
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
202
208
|
filesWrittenThisPlan++;
|
|
203
|
-
return { file:
|
|
209
|
+
return { file: relPath, created: !existed };
|
|
204
210
|
}
|
|
205
211
|
|
|
206
212
|
// -----------------------------------------------------------------
|
|
@@ -212,6 +218,7 @@ export async function executeStep(
|
|
|
212
218
|
}
|
|
213
219
|
assertSafePath(file, projectDir);
|
|
214
220
|
const filePath = path.resolve(projectDir, file);
|
|
221
|
+
const relPath = path.relative(projectDir, filePath);
|
|
215
222
|
|
|
216
223
|
if (!fs.existsSync(filePath)) {
|
|
217
224
|
return { blocked: true, blockReason: `File not found: ${file}` };
|
|
@@ -234,6 +241,17 @@ export async function executeStep(
|
|
|
234
241
|
return { blocked: true, blockReason: 'patch_file requires "patches" array or "find"+"replace" args.' };
|
|
235
242
|
}
|
|
236
243
|
|
|
244
|
+
// Validate each patch entry has string find and replace
|
|
245
|
+
for (let i = 0; i < patches.length; i++) {
|
|
246
|
+
const p = patches[i];
|
|
247
|
+
if (typeof p.find !== 'string') {
|
|
248
|
+
return { blocked: true, blockReason: `patch_file: patch[${i}].find must be a string, got ${typeof p.find}.` };
|
|
249
|
+
}
|
|
250
|
+
if (typeof p.replace !== 'string') {
|
|
251
|
+
return { blocked: true, blockReason: `patch_file: patch[${i}].replace must be a string, got ${typeof p.replace}.` };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
237
255
|
let applied = 0;
|
|
238
256
|
const notFound: string[] = [];
|
|
239
257
|
|
|
@@ -248,7 +266,7 @@ export async function executeStep(
|
|
|
248
266
|
|
|
249
267
|
if (applied === 0) {
|
|
250
268
|
return {
|
|
251
|
-
file:
|
|
269
|
+
file: relPath,
|
|
252
270
|
output: `No patches applied. Search strings not found: ${notFound.join('; ')}`,
|
|
253
271
|
};
|
|
254
272
|
}
|
|
@@ -259,7 +277,7 @@ export async function executeStep(
|
|
|
259
277
|
if (originalSize > SHRINK_GUARD_MIN_SIZE && newSize < originalSize * MAX_SHRINK_RATIO) {
|
|
260
278
|
const shrinkPct = Math.round((1 - newSize / originalSize) * 100);
|
|
261
279
|
return {
|
|
262
|
-
file:
|
|
280
|
+
file: relPath,
|
|
263
281
|
blocked: true,
|
|
264
282
|
blockReason:
|
|
265
283
|
`Refusing to patch ${path.basename(filePath)}: result (${newSize}B) ` +
|
|
@@ -273,7 +291,7 @@ export async function executeStep(
|
|
|
273
291
|
|
|
274
292
|
const summary = `Applied ${applied}/${patches.length} patches` +
|
|
275
293
|
(notFound.length ? `. Not found: ${notFound.join('; ')}` : '');
|
|
276
|
-
return { file:
|
|
294
|
+
return { file: relPath, output: summary };
|
|
277
295
|
}
|
|
278
296
|
|
|
279
297
|
// -----------------------------------------------------------------
|
|
@@ -281,10 +299,11 @@ export async function executeStep(
|
|
|
281
299
|
// -----------------------------------------------------------------
|
|
282
300
|
case OP_READ_FILE: {
|
|
283
301
|
if (!file) {
|
|
284
|
-
return {
|
|
302
|
+
return { blocked: true, blockReason: 'read_file requires a "file" argument.' };
|
|
285
303
|
}
|
|
286
304
|
assertSafePath(file, projectDir);
|
|
287
305
|
const filePath = path.resolve(projectDir, file);
|
|
306
|
+
const relPath = path.relative(projectDir, filePath);
|
|
288
307
|
if (!fs.existsSync(filePath)) {
|
|
289
308
|
return { output: `File not found: ${file}` };
|
|
290
309
|
}
|
|
@@ -298,9 +317,9 @@ export async function executeStep(
|
|
|
298
317
|
return { output: `File too large to read (${stat.size} bytes, max ${MAX_READ_SIZE}). Use run_shell with head/tail instead.` };
|
|
299
318
|
}
|
|
300
319
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
301
|
-
return { file:
|
|
320
|
+
return { file: relPath, output: content };
|
|
302
321
|
} catch (err: unknown) {
|
|
303
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
322
|
+
const msg = (err instanceof Error ? err.message : String(err)).replaceAll(projectDir + '/', '');
|
|
304
323
|
return { output: `Error reading file "${file}": ${msg}` };
|
|
305
324
|
}
|
|
306
325
|
}
|
|
@@ -341,8 +360,9 @@ export async function executeStep(
|
|
|
341
360
|
const stdout = (execErr.stdout ?? '').trim();
|
|
342
361
|
const stderr = (execErr.stderr ?? '').trim();
|
|
343
362
|
const combined = [stdout, stderr].filter(Boolean).join('\n');
|
|
363
|
+
const raw = combined || (err instanceof Error ? err.message : String(err));
|
|
344
364
|
return {
|
|
345
|
-
output:
|
|
365
|
+
output: raw.replaceAll(projectDir + '/', ''),
|
|
346
366
|
};
|
|
347
367
|
}
|
|
348
368
|
}
|
|
@@ -364,7 +384,7 @@ export async function executeStep(
|
|
|
364
384
|
try {
|
|
365
385
|
entries = fs.readdirSync(targetDir, { recursive: true, encoding: 'utf-8' }) as string[];
|
|
366
386
|
} catch (err: unknown) {
|
|
367
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
387
|
+
const msg = (err instanceof Error ? err.message : String(err)).replaceAll(projectDir + '/', '');
|
|
368
388
|
return { output: `Error listing directory "${dir}": ${msg}` };
|
|
369
389
|
}
|
|
370
390
|
let files = entries
|
|
@@ -376,6 +396,9 @@ export async function executeStep(
|
|
|
376
396
|
.sort();
|
|
377
397
|
|
|
378
398
|
if (pattern) {
|
|
399
|
+
if (pattern.length > 200) {
|
|
400
|
+
return { blocked: true, blockReason: `list_files: regex pattern too long (${pattern.length} chars, max 200).` };
|
|
401
|
+
}
|
|
379
402
|
try {
|
|
380
403
|
const regex = new RegExp(pattern);
|
|
381
404
|
files = files.filter(f => regex.test(f));
|
|
@@ -399,7 +422,60 @@ export async function executeStep(
|
|
|
399
422
|
if (!title) return { blocked: true, blockReason: 'task_create requires a "title" argument.' };
|
|
400
423
|
|
|
401
424
|
const store = new TaskStore(projectDir);
|
|
402
|
-
|
|
425
|
+
|
|
426
|
+
// Validate assignedProfile: must be a valid slug (lowercase alphanumeric + hyphens)
|
|
427
|
+
const rawProfile = args.assignedProfile as string | undefined;
|
|
428
|
+
let assignedProfile: string | undefined;
|
|
429
|
+
if (rawProfile !== undefined && rawProfile !== null) {
|
|
430
|
+
if (typeof rawProfile !== 'string' || !rawProfile.trim()) {
|
|
431
|
+
return { blocked: true, blockReason: 'task_create: assignedProfile must be a non-empty string.' };
|
|
432
|
+
}
|
|
433
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(rawProfile)) {
|
|
434
|
+
return { blocked: true, blockReason: `task_create: assignedProfile "${rawProfile}" is not a valid slug (use lowercase alphanumeric with hyphens).` };
|
|
435
|
+
}
|
|
436
|
+
assignedProfile = rawProfile;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Validate complexity against enum, default to 'simple' if invalid
|
|
440
|
+
const VALID_COMPLEXITIES = new Set(['trivial', 'simple', 'moderate', 'complex']);
|
|
441
|
+
const rawComplexity = args.complexity as string | undefined;
|
|
442
|
+
const complexity: 'trivial' | 'simple' | 'moderate' | 'complex' =
|
|
443
|
+
rawComplexity && VALID_COMPLEXITIES.has(rawComplexity)
|
|
444
|
+
? (rawComplexity as 'trivial' | 'simple' | 'moderate' | 'complex')
|
|
445
|
+
: 'simple';
|
|
446
|
+
|
|
447
|
+
// Validate priority: coerce to number, default to 0 if NaN
|
|
448
|
+
const rawPriority = Number(args.priority);
|
|
449
|
+
const priority = Number.isFinite(rawPriority) ? rawPriority : 0;
|
|
450
|
+
|
|
451
|
+
// Validate parentId — must reference an existing task
|
|
452
|
+
const rawParentId = args.parentId as string | undefined;
|
|
453
|
+
let parentId: string | undefined;
|
|
454
|
+
if (rawParentId) {
|
|
455
|
+
// Resolve through symbolicIdMap first (in case it's a title or symbolic ref)
|
|
456
|
+
const resolved = symbolicIdMap?.[rawParentId] ?? rawParentId;
|
|
457
|
+
const parentTask = await store.get(resolved);
|
|
458
|
+
if (!parentTask) {
|
|
459
|
+
return { blocked: true, blockReason: `task_create: parentId "${rawParentId}" does not match any existing task. Use the exact Task ID from the prompt.` };
|
|
460
|
+
}
|
|
461
|
+
parentId = resolved;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Idempotent: if a task with the same title and parentId already exists, return it
|
|
465
|
+
// instead of creating a duplicate. This prevents retry loops from producing duplicates.
|
|
466
|
+
if (parentId) {
|
|
467
|
+
const existing = (await store.list()).find(
|
|
468
|
+
t => t.parentId === parentId && t.title.toLowerCase() === title.toLowerCase(),
|
|
469
|
+
);
|
|
470
|
+
if (existing) {
|
|
471
|
+
if (symbolicIdMap) {
|
|
472
|
+
const symbolicKey = (args.symbolicId as string) ?? (args.id as string);
|
|
473
|
+
if (symbolicKey) symbolicIdMap[symbolicKey] = existing.id;
|
|
474
|
+
symbolicIdMap[title] = existing.id;
|
|
475
|
+
}
|
|
476
|
+
return { output: `Task "${title}" already exists (${existing.id}), skipped duplicate.` };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
403
479
|
|
|
404
480
|
// Resolve symbolic IDs in dependsOn through the map
|
|
405
481
|
const rawDeps = (args.dependsOn as string[]) ?? [];
|
|
@@ -410,9 +486,9 @@ export async function executeStep(
|
|
|
410
486
|
const input: CreateTaskInput = {
|
|
411
487
|
title,
|
|
412
488
|
description: (args.description as string) ?? title,
|
|
413
|
-
complexity
|
|
414
|
-
priority
|
|
415
|
-
parentId
|
|
489
|
+
complexity,
|
|
490
|
+
priority,
|
|
491
|
+
parentId,
|
|
416
492
|
dependsOn: resolvedDeps,
|
|
417
493
|
assignedProfile,
|
|
418
494
|
};
|
|
@@ -420,16 +496,30 @@ export async function executeStep(
|
|
|
420
496
|
// Support inline subtasks
|
|
421
497
|
const subtasksArg = args.subtasks as Array<Record<string, unknown>> | undefined;
|
|
422
498
|
if (subtasksArg?.length) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
499
|
+
// Validate subtask titles are non-empty
|
|
500
|
+
for (let i = 0; i < subtasksArg.length; i++) {
|
|
501
|
+
const subTitle = (subtasksArg[i].title as string) ?? '';
|
|
502
|
+
if (!subTitle.trim()) {
|
|
503
|
+
return { blocked: true, blockReason: `task_create: subtask[${i}] has an empty title. All subtasks must have non-empty titles.` };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
input.subtasks = subtasksArg.map((s) => {
|
|
508
|
+
const subComplexity = s.complexity as string | undefined;
|
|
509
|
+
const subPriority = Number(s.priority);
|
|
510
|
+
return {
|
|
511
|
+
title: (s.title as string) ?? '',
|
|
512
|
+
description: (s.description as string) ?? '',
|
|
513
|
+
complexity: subComplexity && VALID_COMPLEXITIES.has(subComplexity)
|
|
514
|
+
? (subComplexity as 'trivial' | 'simple' | 'moderate' | 'complex')
|
|
515
|
+
: 'simple',
|
|
516
|
+
priority: Number.isFinite(subPriority) ? subPriority : 0,
|
|
517
|
+
assignedProfile: (s.assignedProfile as string) ?? assignedProfile ?? 'developer',
|
|
518
|
+
dependsOn: symbolicIdMap
|
|
519
|
+
? ((s.dependsOn as string[]) ?? []).map(dep => symbolicIdMap[dep] ?? dep)
|
|
520
|
+
: (s.dependsOn as string[]) ?? [],
|
|
521
|
+
};
|
|
522
|
+
});
|
|
433
523
|
}
|
|
434
524
|
|
|
435
525
|
const task = await store.create(input);
|