@synergenius/flow-weaver-pack-weaver 0.9.199 → 0.9.201
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 +5 -5
- package/dist/ai-chat-provider.js.map +1 -1
- package/dist/bot/acceptance-merge.d.ts +21 -0
- package/dist/bot/acceptance-merge.d.ts.map +1 -0
- package/dist/bot/acceptance-merge.js +46 -0
- package/dist/bot/acceptance-merge.js.map +1 -0
- package/dist/bot/ai-client.d.ts +14 -2
- package/dist/bot/ai-client.d.ts.map +1 -1
- package/dist/bot/ai-client.js +71 -24
- package/dist/bot/ai-client.js.map +1 -1
- package/dist/bot/assistant-tools.js +3 -3
- package/dist/bot/assistant-tools.js.map +1 -1
- package/dist/bot/audit-logger.d.ts.map +1 -1
- package/dist/bot/audit-logger.js +34 -14
- package/dist/bot/audit-logger.js.map +1 -1
- package/dist/bot/audit-trail.d.ts +67 -0
- package/dist/bot/audit-trail.d.ts.map +1 -0
- package/dist/bot/audit-trail.js +153 -0
- package/dist/bot/audit-trail.js.map +1 -0
- package/dist/bot/behavior-defaults.d.ts +1 -1
- package/dist/bot/behavior-defaults.d.ts.map +1 -1
- package/dist/bot/behavior-defaults.js +7 -3
- package/dist/bot/behavior-defaults.js.map +1 -1
- package/dist/bot/capability-registry.d.ts +9 -0
- package/dist/bot/capability-registry.d.ts.map +1 -1
- package/dist/bot/capability-registry.js +81 -27
- package/dist/bot/capability-registry.js.map +1 -1
- package/dist/bot/capability-types.d.ts +10 -0
- package/dist/bot/capability-types.d.ts.map +1 -1
- package/dist/bot/cli-provider.d.ts.map +1 -1
- package/dist/bot/cli-provider.js +8 -7
- package/dist/bot/cli-provider.js.map +1 -1
- package/dist/bot/preflight.d.ts +48 -0
- package/dist/bot/preflight.d.ts.map +1 -0
- package/dist/bot/preflight.js +247 -0
- package/dist/bot/preflight.js.map +1 -0
- package/dist/bot/provider-shim.d.ts +74 -0
- package/dist/bot/provider-shim.d.ts.map +1 -0
- package/dist/bot/provider-shim.js +176 -0
- package/dist/bot/provider-shim.js.map +1 -0
- package/dist/bot/runner.d.ts +2 -0
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +60 -17
- package/dist/bot/runner.js.map +1 -1
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +72 -115
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/swarm-controller.d.ts +2 -0
- package/dist/bot/swarm-controller.d.ts.map +1 -1
- package/dist/bot/swarm-controller.js +92 -20
- package/dist/bot/swarm-controller.js.map +1 -1
- package/dist/bot/task-create-handler.d.ts +37 -0
- package/dist/bot/task-create-handler.d.ts.map +1 -0
- package/dist/bot/task-create-handler.js +124 -0
- package/dist/bot/task-create-handler.js.map +1 -0
- package/dist/bot/task-store.d.ts +1 -0
- package/dist/bot/task-store.d.ts.map +1 -1
- package/dist/bot/task-store.js +67 -0
- package/dist/bot/task-store.js.map +1 -1
- package/dist/bot/types.d.ts +1 -1
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/bot/weaver-tools.d.ts.map +1 -1
- package/dist/bot/weaver-tools.js +7 -39
- package/dist/bot/weaver-tools.js.map +1 -1
- package/dist/node-types/agent-execute.d.ts +25 -8
- package/dist/node-types/agent-execute.d.ts.map +1 -1
- package/dist/node-types/agent-execute.js +89 -23
- 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 +24 -3
- package/dist/node-types/bot-report.js.map +1 -1
- package/dist/node-types/plan-task.d.ts +8 -17
- package/dist/node-types/plan-task.d.ts.map +1 -1
- package/dist/node-types/plan-task.js +217 -256
- package/dist/node-types/plan-task.js.map +1 -1
- package/dist/node-types/review-result.js +8 -6
- package/dist/node-types/review-result.js.map +1 -1
- package/dist/palindrome.d.ts +9 -0
- package/dist/palindrome.d.ts.map +1 -0
- package/dist/palindrome.js +14 -0
- package/dist/palindrome.js.map +1 -0
- package/dist/ui/approval-card.js +91 -82
- package/dist/ui/bot-activity.js +73 -56
- package/dist/ui/bot-config.js +48 -31
- package/dist/ui/bot-dashboard.js +52 -36
- package/dist/ui/bot-panel.js +230 -228
- package/dist/ui/bot-slot-card.js +100 -90
- package/dist/ui/bot-status.js +37 -15
- package/dist/ui/budget-bar.js +57 -31
- package/dist/ui/capability-editor.js +447 -378
- package/dist/ui/chat-task-result.js +78 -71
- package/dist/ui/decision-log.js +68 -81
- package/dist/ui/genesis-block.js +86 -95
- package/dist/ui/instance-stream-view.js +722 -0
- package/dist/ui/profile-card.js +96 -221
- package/dist/ui/profile-editor.js +532 -575
- package/dist/ui/settings-section.js +41 -45
- package/dist/ui/swarm-controls.js +212 -135
- package/dist/ui/swarm-dashboard.js +3992 -2715
- package/dist/ui/task-detail-view.js +415 -521
- package/dist/ui/task-editor.js +339 -390
- package/dist/ui/task-pool-list.js +60 -55
- package/dist/workflows/src/palindrome.d.ts +11 -0
- package/dist/workflows/src/palindrome.d.ts.map +1 -0
- package/dist/workflows/src/palindrome.js +16 -0
- package/dist/workflows/src/palindrome.js.map +1 -0
- package/dist/workflows/tests/palindrome.test.d.ts +2 -0
- package/dist/workflows/tests/palindrome.test.d.ts.map +1 -0
- package/dist/workflows/tests/palindrome.test.js +41 -0
- package/dist/workflows/tests/palindrome.test.js.map +1 -0
- package/dist/workflows/weaver-bot-batch.js +1 -1
- package/dist/workflows/weaver-bot-batch.js.map +1 -1
- package/dist/workflows/weaver-bot.js +1 -1
- package/dist/workflows/weaver-bot.js.map +1 -1
- package/flowweaver.manifest.json +1 -1
- package/package.json +8 -2
- package/src/ai-chat-provider.ts +5 -5
- package/src/bot/acceptance-merge.ts +62 -0
- package/src/bot/ai-client.ts +77 -21
- package/src/bot/assistant-tools.ts +3 -3
- package/src/bot/audit-logger.ts +42 -14
- package/src/bot/audit-trail.ts +211 -0
- package/src/bot/behavior-defaults.ts +7 -2
- package/src/bot/capability-registry.ts +84 -28
- package/src/bot/capability-types.ts +11 -0
- package/src/bot/cli-provider.ts +8 -7
- package/src/bot/preflight.ts +285 -0
- package/src/bot/provider-shim.ts +218 -0
- package/src/bot/runner.ts +68 -20
- package/src/bot/step-executor.ts +69 -127
- package/src/bot/swarm-controller.ts +94 -20
- package/src/bot/task-create-handler.ts +164 -0
- package/src/bot/task-store.ts +83 -0
- package/src/bot/types.ts +4 -1
- package/src/bot/weaver-tools.ts +7 -45
- package/src/node-types/agent-execute.ts +102 -16
- package/src/node-types/bot-report.ts +24 -3
- package/src/node-types/plan-task.ts +238 -280
- package/src/node-types/review-result.ts +8 -6
- package/src/palindrome.ts +14 -0
- package/src/ui/approval-card.tsx +78 -62
- package/src/ui/bot-activity.tsx +12 -10
- package/src/ui/bot-config.tsx +12 -10
- package/src/ui/bot-dashboard.tsx +13 -11
- package/src/ui/bot-panel.tsx +189 -171
- package/src/ui/bot-slot-card.tsx +125 -70
- package/src/ui/bot-status.tsx +4 -4
- package/src/ui/budget-bar.tsx +86 -25
- package/src/ui/capability-editor.tsx +392 -257
- package/src/ui/chat-task-result.tsx +81 -78
- package/src/ui/decision-log.tsx +76 -73
- package/src/ui/genesis-block.tsx +91 -61
- package/src/ui/instance-stream-view.tsx +861 -0
- package/src/ui/profile-card.tsx +195 -168
- package/src/ui/profile-editor.tsx +453 -370
- package/src/ui/settings-section.tsx +46 -39
- package/src/ui/swarm-controls.tsx +252 -123
- package/src/ui/swarm-dashboard.tsx +999 -466
- package/src/ui/task-detail-view.tsx +485 -428
- package/src/ui/task-editor.tsx +329 -271
- package/src/ui/task-pool-list.tsx +68 -62
- package/src/workflows/src/palindrome.ts +16 -0
- package/src/workflows/tests/palindrome.test.ts +49 -0
- package/src/workflows/weaver-bot-batch.ts +1 -1
- package/src/workflows/weaver-bot.ts +1 -1
- package/dist/ui/bot-constants.d.ts +0 -14
- package/dist/ui/bot-constants.d.ts.map +0 -1
- package/dist/ui/bot-constants.js +0 -189
- package/dist/ui/bot-constants.js.map +0 -1
- package/dist/ui/steer-api.d.ts +0 -7
- package/dist/ui/steer-api.d.ts.map +0 -1
- package/dist/ui/steer-api.js +0 -11
- package/dist/ui/steer-api.js.map +0 -1
- package/dist/ui/trace-to-timeline.d.ts +0 -91
- package/dist/ui/trace-to-timeline.d.ts.map +0 -1
- package/dist/ui/trace-to-timeline.js +0 -116
- package/dist/ui/trace-to-timeline.js.map +0 -1
- package/dist/ui/use-stream-timeline.d.ts +0 -50
- package/dist/ui/use-stream-timeline.d.ts.map +0 -1
- package/dist/ui/use-stream-timeline.js +0 -245
- package/dist/ui/use-stream-timeline.js.map +0 -1
|
@@ -234,6 +234,36 @@ export class SwarmController {
|
|
|
234
234
|
this.frozenPromptPrefix = null;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
// Pre-flight validation — catch tool pipeline config bugs before spending money
|
|
238
|
+
try {
|
|
239
|
+
const { runPreflight, runBridgePreflight, formatPreflightResult } = await import('./preflight.js');
|
|
240
|
+
|
|
241
|
+
// Static checks (schemas, handlers, modes)
|
|
242
|
+
const preflight = runPreflight();
|
|
243
|
+
if (!preflight.passed) {
|
|
244
|
+
const report = formatPreflightResult(preflight);
|
|
245
|
+
console.error(report);
|
|
246
|
+
this.eventLog.emit({ type: 'swarm-preflight-failed', timestamp: Date.now(), data: { errors: preflight.errors } });
|
|
247
|
+
throw new Error(`Swarm preflight failed: ${preflight.errors.length} error(s). Fix before starting.`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Runtime check (MCP bridge connectivity)
|
|
251
|
+
const bridgePreflight = await runBridgePreflight();
|
|
252
|
+
const totalChecks = preflight.checks.length + bridgePreflight.checks.length;
|
|
253
|
+
if (!bridgePreflight.passed) {
|
|
254
|
+
const report = formatPreflightResult(bridgePreflight);
|
|
255
|
+
console.error(report);
|
|
256
|
+
this.eventLog.emit({ type: 'swarm-preflight-failed', timestamp: Date.now(), data: { errors: bridgePreflight.errors } });
|
|
257
|
+
throw new Error(`Swarm preflight failed: MCP bridge connectivity check failed. ${bridgePreflight.errors.map(e => e.message).join('; ')}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.log(`\x1b[32m[swarm-preflight] ✓ Tool pipeline validated (${totalChecks} checks passed)\x1b[0m`);
|
|
261
|
+
} catch (err: unknown) {
|
|
262
|
+
if (err instanceof Error && err.message.startsWith('Swarm preflight failed')) throw err;
|
|
263
|
+
// Non-fatal: preflight import failure shouldn't block swarm
|
|
264
|
+
if (process.env.WEAVER_VERBOSE) console.warn('[swarm] preflight check skipped:', err);
|
|
265
|
+
}
|
|
266
|
+
|
|
237
267
|
console.log(`\x1b[36m[swarm] started (pack-weaver v${PACK_VERSION})\x1b[0m`);
|
|
238
268
|
this.eventLog.emit({ type: 'swarm-started', timestamp: Date.now(), data: { packVersion: PACK_VERSION } });
|
|
239
269
|
|
|
@@ -295,6 +325,14 @@ export class SwarmController {
|
|
|
295
325
|
this.settledPromises.clear();
|
|
296
326
|
|
|
297
327
|
this.eventLog.emit({ type: 'swarm-stopped', timestamp: Date.now() });
|
|
328
|
+
|
|
329
|
+
// Assemble complete audit trail — one file with the full story
|
|
330
|
+
try {
|
|
331
|
+
const { assembleAuditTrail } = await import('./audit-trail.js');
|
|
332
|
+
const trail = assembleAuditTrail(this.projectDir);
|
|
333
|
+
const trailPath = path.join(this.projectDir, '.weaver', 'audit-trail.json');
|
|
334
|
+
fs.writeFileSync(trailPath, JSON.stringify(trail, null, 2));
|
|
335
|
+
} catch { /* non-fatal */ }
|
|
298
336
|
}
|
|
299
337
|
|
|
300
338
|
getStatus(): SwarmState {
|
|
@@ -344,6 +382,20 @@ export class SwarmController {
|
|
|
344
382
|
// Token/cost recording
|
|
345
383
|
// -----------------------------------------------------------------------
|
|
346
384
|
|
|
385
|
+
/** Read provider from .weaver.json, fall back to 'auto'. */
|
|
386
|
+
private _resolveProvider(): string {
|
|
387
|
+
try {
|
|
388
|
+
const configPath = path.join(this.projectDir, '.weaver.json');
|
|
389
|
+
if (fs.existsSync(configPath)) {
|
|
390
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
391
|
+
if (config.provider && typeof config.provider === 'string') {
|
|
392
|
+
return config.provider;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
} catch { /* malformed config — fall back */ }
|
|
396
|
+
return 'auto';
|
|
397
|
+
}
|
|
398
|
+
|
|
347
399
|
private _buildBasicRunProgress(
|
|
348
400
|
runId: string, workerId: string, profileId: string,
|
|
349
401
|
result: { success?: boolean; summary?: string },
|
|
@@ -353,8 +405,9 @@ export class SwarmController {
|
|
|
353
405
|
let outcome: 'completed' | 'contributed' | 'stalled' | 'crashed';
|
|
354
406
|
if (result.success) {
|
|
355
407
|
outcome = 'completed';
|
|
356
|
-
} else if (tokensUsed === 0
|
|
357
|
-
//
|
|
408
|
+
} else if (tokensUsed === 0) {
|
|
409
|
+
// Zero tokens consumed — AI call either failed immediately or timed out.
|
|
410
|
+
// Always stalled regardless of duration (a 600s timeout with 0 tokens is not "contributed").
|
|
358
411
|
outcome = 'stalled';
|
|
359
412
|
} else {
|
|
360
413
|
outcome = 'contributed';
|
|
@@ -375,27 +428,40 @@ export class SwarmController {
|
|
|
375
428
|
};
|
|
376
429
|
}
|
|
377
430
|
|
|
378
|
-
private async _checkAcceptance(task: Task): Promise<import('./task-types.js').AcceptanceResult> {
|
|
379
|
-
|
|
380
|
-
|
|
431
|
+
private async _checkAcceptance(task: Task, profileId?: string): Promise<import('./task-types.js').AcceptanceResult> {
|
|
432
|
+
// Merge task-specific checks with capability-level checks
|
|
433
|
+
const { mergeAcceptanceChecks } = await import('./acceptance-merge.js');
|
|
434
|
+
const { getCapabilityAcceptanceChecks } = await import('./capability-registry.js');
|
|
435
|
+
|
|
436
|
+
const taskChecks = task.acceptance?.checks ?? [];
|
|
437
|
+
const capChecks = profileId ? getCapabilityAcceptanceChecks(profileId) : [];
|
|
438
|
+
const allChecks = mergeAcceptanceChecks(taskChecks, capChecks, this.projectDir);
|
|
439
|
+
|
|
440
|
+
if (allChecks.length === 0) {
|
|
381
441
|
return { met: true, results: [], checkedAt: new Date().toISOString() };
|
|
382
442
|
}
|
|
383
443
|
|
|
384
444
|
const { execSync } = await import('node:child_process');
|
|
385
445
|
const results: Array<{ name: string; pass: boolean; detail?: string }> = [];
|
|
386
446
|
|
|
387
|
-
for (const check of
|
|
447
|
+
for (const check of allChecks) {
|
|
388
448
|
try {
|
|
389
|
-
execSync(check.command, {
|
|
449
|
+
const stdout = execSync(check.command, {
|
|
390
450
|
cwd: this.projectDir,
|
|
391
451
|
encoding: 'utf-8',
|
|
392
452
|
timeout: 60_000,
|
|
393
453
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
394
454
|
});
|
|
395
|
-
|
|
455
|
+
console.log(`\x1b[32m ✓ ${check.name}\x1b[0m`);
|
|
456
|
+
results.push({ name: check.name, pass: true, detail: stdout?.trim() || undefined });
|
|
396
457
|
} catch (err: unknown) {
|
|
397
|
-
const e = err as { stdout?: string; stderr?: string; message?: string };
|
|
398
|
-
const
|
|
458
|
+
const e = err as { stdout?: string; stderr?: string; message?: string; status?: number };
|
|
459
|
+
const stdout = e.stdout ?? '';
|
|
460
|
+
const stderr = e.stderr ?? '';
|
|
461
|
+
const detail = `exit ${e.status ?? '?'}\nstdout: ${stdout}\nstderr: ${stderr}`;
|
|
462
|
+
console.log(`\x1b[31m ✗ ${check.name} (exit ${e.status ?? '?'})\x1b[0m`);
|
|
463
|
+
if (stdout.trim()) console.log(` stdout: ${stdout.trim().slice(0, 300)}`);
|
|
464
|
+
if (stderr.trim()) console.log(` stderr: ${stderr.trim().slice(0, 300)}`);
|
|
399
465
|
results.push({ name: check.name, pass: false, detail });
|
|
400
466
|
}
|
|
401
467
|
}
|
|
@@ -689,7 +755,7 @@ export class SwarmController {
|
|
|
689
755
|
this.eventLog.emit({
|
|
690
756
|
type: 'orchestrator-decision',
|
|
691
757
|
timestamp: Date.now(),
|
|
692
|
-
data: { taskId: d.taskId,
|
|
758
|
+
data: { taskId: d.taskId, taskTitle: d.taskTitle, assignedProfileId: d.assignedProfileId, assignedInstanceId: d.assignedInstanceId, method: d.method, reason: d.reason, confidence: d.confidence },
|
|
693
759
|
});
|
|
694
760
|
}
|
|
695
761
|
}
|
|
@@ -886,24 +952,34 @@ export class SwarmController {
|
|
|
886
952
|
// Trivial tasks get cheaper models and fewer retries; complex tasks are unchanged.
|
|
887
953
|
const baseBehavior: ProfileBehavior = profile.preferences.behavior
|
|
888
954
|
?? buildDefaultBehavior(profile.preferences.costStrategy, undefined, profile.id);
|
|
889
|
-
const behavior = adjustBehaviorForComplexity(baseBehavior, task.complexity);
|
|
955
|
+
const behavior = adjustBehaviorForComplexity(baseBehavior, task.complexity, profile.id);
|
|
890
956
|
const behaviorJson = JSON.stringify(behavior);
|
|
891
957
|
|
|
892
958
|
// Create per-run event log in the workspace .weaver directory
|
|
893
959
|
// (must match the path used by fw_weaver_events in ai-chat-provider)
|
|
894
960
|
const runEventLog = new EventLog(runId, path.join(this.projectDir, '.weaver'));
|
|
895
961
|
|
|
896
|
-
// Execute workflow — resolve relative filePath against project dir
|
|
897
|
-
|
|
962
|
+
// Execute workflow — resolve relative filePath against project dir.
|
|
963
|
+
// Fallback: if not found (e.g., npm install pruned pack symlinks),
|
|
964
|
+
// resolve from the pack's own root since we always know where our files are.
|
|
965
|
+
let workflowPath = path.isAbsolute(bot.filePath)
|
|
898
966
|
? bot.filePath
|
|
899
967
|
: path.resolve(this.projectDir, bot.filePath);
|
|
968
|
+
if (!fs.existsSync(workflowPath)) {
|
|
969
|
+
const packRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
970
|
+
const stripped = bot.filePath.replace(/^node_modules\/@synergenius\/flow-weaver-pack-weaver\//, '');
|
|
971
|
+
const fallback = path.resolve(packRoot, stripped);
|
|
972
|
+
if (fs.existsSync(fallback)) {
|
|
973
|
+
workflowPath = fallback;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
900
976
|
// No swarm-level timeout race — the AI call timeout (10min) in the worker
|
|
901
977
|
// is the only boundary. This prevents orphaned runs from timeout races.
|
|
902
978
|
result = await runWorkflow(workflowPath, {
|
|
903
979
|
runId,
|
|
904
980
|
taskId,
|
|
905
981
|
botId: workerId,
|
|
906
|
-
config: { provider:
|
|
982
|
+
config: { provider: this._resolveProvider() },
|
|
907
983
|
params: {
|
|
908
984
|
taskJson,
|
|
909
985
|
projectDir: this.projectDir,
|
|
@@ -967,12 +1043,10 @@ export class SwarmController {
|
|
|
967
1043
|
let releaseStatus: 'done' | 'open' = 'open';
|
|
968
1044
|
|
|
969
1045
|
if (result.success) {
|
|
970
|
-
if
|
|
971
|
-
|
|
972
|
-
} else {
|
|
1046
|
+
// Always run acceptance — capability checks may exist even if task has no checks
|
|
1047
|
+
{
|
|
973
1048
|
try {
|
|
974
|
-
|
|
975
|
-
const acceptResult = await this._checkAcceptance(task);
|
|
1049
|
+
const acceptResult = await this._checkAcceptance(task!, profile.id);
|
|
976
1050
|
if (acceptResult.met) {
|
|
977
1051
|
releaseStatus = 'done';
|
|
978
1052
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared task_create handler — single source of truth for creating tasks.
|
|
3
|
+
*
|
|
4
|
+
* Used by both step-executor (plan-task agent loop) and weaver-tools (MCP bridge).
|
|
5
|
+
* Handles @self resolution, dedup, acceptance parsing, validation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CreateTaskInput } from './task-types.js';
|
|
9
|
+
import { TaskStore } from './task-store.js';
|
|
10
|
+
|
|
11
|
+
export interface TaskCreateArgs {
|
|
12
|
+
title?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
parentId?: string;
|
|
15
|
+
assignedProfile?: string;
|
|
16
|
+
complexity?: string;
|
|
17
|
+
priority?: number;
|
|
18
|
+
dependsOn?: string[];
|
|
19
|
+
acceptance?: { checks?: Array<{ name: string; command: string }> };
|
|
20
|
+
files?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TaskCreateResult {
|
|
24
|
+
output: string;
|
|
25
|
+
blocked?: boolean;
|
|
26
|
+
blockReason?: string;
|
|
27
|
+
taskId?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a task with full validation, @self resolution, and dedup.
|
|
32
|
+
*
|
|
33
|
+
* @param args - The task_create arguments from the AI model
|
|
34
|
+
* @param projectDir - Workspace directory
|
|
35
|
+
* @param symbolicIdMap - Optional map for resolving symbolic IDs (step-executor path)
|
|
36
|
+
*/
|
|
37
|
+
export async function handleTaskCreate(
|
|
38
|
+
args: TaskCreateArgs,
|
|
39
|
+
projectDir: string,
|
|
40
|
+
symbolicIdMap?: Record<string, string>,
|
|
41
|
+
): Promise<TaskCreateResult> {
|
|
42
|
+
const title = String(args.title ?? '');
|
|
43
|
+
if (!title.trim()) {
|
|
44
|
+
return { output: '', blocked: true, blockReason: 'task_create requires a "title" argument.' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const store = new TaskStore(projectDir);
|
|
48
|
+
|
|
49
|
+
// Validate assignedProfile
|
|
50
|
+
const rawProfile = args.assignedProfile;
|
|
51
|
+
let assignedProfile: string | undefined;
|
|
52
|
+
if (rawProfile !== undefined && rawProfile !== null) {
|
|
53
|
+
if (typeof rawProfile !== 'string' || !rawProfile.trim()) {
|
|
54
|
+
return { output: '', blocked: true, blockReason: 'task_create: assignedProfile must be a non-empty string.' };
|
|
55
|
+
}
|
|
56
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(rawProfile)) {
|
|
57
|
+
return { output: '', blocked: true, blockReason: `task_create: assignedProfile "${rawProfile}" is not a valid slug.` };
|
|
58
|
+
}
|
|
59
|
+
assignedProfile = rawProfile;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Validate complexity
|
|
63
|
+
const VALID_COMPLEXITIES = new Set(['trivial', 'simple', 'moderate', 'complex']);
|
|
64
|
+
const rawComplexity = args.complexity;
|
|
65
|
+
const complexity: 'trivial' | 'simple' | 'moderate' | 'complex' =
|
|
66
|
+
rawComplexity && VALID_COMPLEXITIES.has(rawComplexity)
|
|
67
|
+
? (rawComplexity as 'trivial' | 'simple' | 'moderate' | 'complex')
|
|
68
|
+
: 'simple';
|
|
69
|
+
|
|
70
|
+
// Validate priority
|
|
71
|
+
const rawPriority = Number(args.priority);
|
|
72
|
+
const priority = Number.isFinite(rawPriority) ? rawPriority : 0;
|
|
73
|
+
|
|
74
|
+
// Resolve parentId — handle @self symbolic reference
|
|
75
|
+
let parentId: string | undefined;
|
|
76
|
+
const rawParentId = args.parentId;
|
|
77
|
+
if (rawParentId) {
|
|
78
|
+
if (rawParentId === '@self') {
|
|
79
|
+
// Try symbolicIdMap first (step-executor path), then fallback to store lookup
|
|
80
|
+
const fromMap = symbolicIdMap?.['@self'];
|
|
81
|
+
if (fromMap) {
|
|
82
|
+
parentId = fromMap;
|
|
83
|
+
} else {
|
|
84
|
+
// MCP bridge path — no symbolicIdMap, resolve from task store
|
|
85
|
+
const all = await store.list();
|
|
86
|
+
const inProgress = all.find(t => t.status === 'in-progress' && t.assignedProfile === 'orchestrator');
|
|
87
|
+
if (inProgress) {
|
|
88
|
+
parentId = inProgress.id;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
// Resolve through symbolicIdMap or use as-is
|
|
93
|
+
parentId = symbolicIdMap?.[rawParentId] ?? rawParentId;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Validate parent exists
|
|
97
|
+
if (parentId) {
|
|
98
|
+
const parentTask = await store.get(parentId);
|
|
99
|
+
if (!parentTask) {
|
|
100
|
+
return { output: '', blocked: true, blockReason: `task_create: parentId "${rawParentId}" does not match any existing task.` };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Idempotent: skip if a task with the same title+parent already exists
|
|
106
|
+
if (parentId) {
|
|
107
|
+
const all = await store.list();
|
|
108
|
+
const existing = all.find(
|
|
109
|
+
t => t.parentId === parentId && t.title.toLowerCase() === title.toLowerCase(),
|
|
110
|
+
);
|
|
111
|
+
if (existing) {
|
|
112
|
+
if (symbolicIdMap) {
|
|
113
|
+
symbolicIdMap[title] = existing.id;
|
|
114
|
+
}
|
|
115
|
+
return { output: `Task "${title}" already exists (${existing.id}), skipped duplicate.`, taskId: existing.id };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Resolve symbolic IDs in dependsOn
|
|
120
|
+
const rawDeps = args.dependsOn ?? [];
|
|
121
|
+
const resolvedDeps = symbolicIdMap
|
|
122
|
+
? rawDeps.map(dep => symbolicIdMap[dep] ?? dep)
|
|
123
|
+
: rawDeps;
|
|
124
|
+
|
|
125
|
+
// Parse acceptance criteria
|
|
126
|
+
const rawAcceptance = args.acceptance;
|
|
127
|
+
const acceptance = rawAcceptance?.checks ? {
|
|
128
|
+
checks: rawAcceptance.checks.map(c => ({
|
|
129
|
+
name: String(c.name ?? ''),
|
|
130
|
+
command: String(c.command ?? ''),
|
|
131
|
+
})).filter(c => c.name && c.command),
|
|
132
|
+
} : undefined;
|
|
133
|
+
|
|
134
|
+
const input: CreateTaskInput = {
|
|
135
|
+
title,
|
|
136
|
+
description: String(args.description ?? title),
|
|
137
|
+
complexity,
|
|
138
|
+
priority,
|
|
139
|
+
parentId,
|
|
140
|
+
dependsOn: resolvedDeps,
|
|
141
|
+
assignedProfile,
|
|
142
|
+
acceptance,
|
|
143
|
+
createdBy: 'ai',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const task = await store.create(input);
|
|
147
|
+
|
|
148
|
+
// Track in symbolicIdMap for subsequent task_create calls
|
|
149
|
+
if (symbolicIdMap) {
|
|
150
|
+
symbolicIdMap[title] = task.id;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Set files if provided
|
|
154
|
+
if (args.files && Array.isArray(args.files)) {
|
|
155
|
+
await store.update(task.id, {
|
|
156
|
+
context: { ...task.context, files: args.files as string[] },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
output: JSON.stringify({ id: task.id, title: task.title, status: task.status }),
|
|
162
|
+
taskId: task.id,
|
|
163
|
+
};
|
|
164
|
+
}
|
package/src/bot/task-store.ts
CHANGED
|
@@ -165,6 +165,54 @@ export class TaskStore {
|
|
|
165
165
|
|
|
166
166
|
const task = tasks[idx];
|
|
167
167
|
|
|
168
|
+
// Guard: never reopen a done or cancelled task via a generic update.
|
|
169
|
+
// Only release() and explicit retry should change terminal status.
|
|
170
|
+
const isTerminal = task.status === 'done' || task.status === 'cancelled';
|
|
171
|
+
if (isTerminal && patch.status && patch.status !== task.status) {
|
|
172
|
+
delete patch.status;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Guard: update() cannot set status to 'done' — must go through release()
|
|
176
|
+
if (patch.status === 'done') {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Cannot transition task ${id} to done via update() — use release() instead`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Guard: completedAt consistency — only done tasks may have completedAt.
|
|
183
|
+
// Cannot set it on non-done tasks, cannot strip it from done tasks.
|
|
184
|
+
if (patch.completedAt !== undefined) {
|
|
185
|
+
if (task.status !== 'done') {
|
|
186
|
+
delete patch.completedAt;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (isTerminal && task.status === 'done' && 'completedAt' in patch && !patch.completedAt) {
|
|
190
|
+
delete patch.completedAt;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Deep-merge context: patch.context is merged INTO task.context,
|
|
194
|
+
// never replaces it wholesale. This prevents accidental loss of
|
|
195
|
+
// runHistory, budgetExhausted, stagnationCount, etc.
|
|
196
|
+
if (patch.context) {
|
|
197
|
+
const patchCtx = patch.context;
|
|
198
|
+
|
|
199
|
+
// Guard: budgetExhausted is one-way — once set, cannot be cleared
|
|
200
|
+
if (task.context.budgetExhausted === true && patchCtx.budgetExhausted === false) {
|
|
201
|
+
patchCtx.budgetExhausted = true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Guard: runHistory is append-only — cannot replace with a shorter array
|
|
205
|
+
if (patchCtx.runHistory) {
|
|
206
|
+
if (patchCtx.runHistory.length < task.context.runHistory.length) {
|
|
207
|
+
patchCtx.runHistory = task.context.runHistory;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Merge into existing context instead of replacing
|
|
212
|
+
Object.assign(task.context, patchCtx);
|
|
213
|
+
delete patch.context;
|
|
214
|
+
}
|
|
215
|
+
|
|
168
216
|
// Reset stagnation when profile is reassigned (fresh start for new approach)
|
|
169
217
|
if (patch.assignedProfile && patch.assignedProfile !== task.assignedProfile) {
|
|
170
218
|
task.context.stagnationCount = 0;
|
|
@@ -253,6 +301,13 @@ export class TaskStore {
|
|
|
253
301
|
|
|
254
302
|
const task = tasks[idx];
|
|
255
303
|
|
|
304
|
+
// Guard: only in-progress tasks can be released
|
|
305
|
+
if (task.status !== 'in-progress') {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`Task ${taskId} is not in-progress (status: ${task.status}) — cannot release`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
256
311
|
// Append run progress
|
|
257
312
|
task.context.runHistory.push(runProgress);
|
|
258
313
|
|
|
@@ -318,6 +373,34 @@ export class TaskStore {
|
|
|
318
373
|
});
|
|
319
374
|
}
|
|
320
375
|
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// Retry — explicit reopen of a terminal task
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
async retry(taskId: string): Promise<Task> {
|
|
381
|
+
return this.mutex.runExclusive(async () => {
|
|
382
|
+
const tasks = this._readAll();
|
|
383
|
+
const idx = tasks.findIndex(t => t.id === taskId);
|
|
384
|
+
if (idx === -1) throw new Error(`Task not found: ${taskId}`);
|
|
385
|
+
|
|
386
|
+
const task = tasks[idx];
|
|
387
|
+
if (task.status !== 'done' && task.status !== 'cancelled') {
|
|
388
|
+
throw new Error(`Task ${taskId} is not in a terminal state (status: ${task.status}) — cannot retry`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
task.status = 'open';
|
|
392
|
+
task.activeRunId = undefined;
|
|
393
|
+
task.completedAt = undefined;
|
|
394
|
+
task.cancelledAt = undefined;
|
|
395
|
+
task.cancelReason = undefined;
|
|
396
|
+
task.updatedAt = new Date().toISOString();
|
|
397
|
+
|
|
398
|
+
tasks[idx] = task;
|
|
399
|
+
this._writeAll(tasks);
|
|
400
|
+
return task;
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
321
404
|
// ---------------------------------------------------------------------------
|
|
322
405
|
// Dependency + parent cascading effects
|
|
323
406
|
// ---------------------------------------------------------------------------
|
package/src/bot/types.ts
CHANGED
|
@@ -709,7 +709,10 @@ export type AuditEventType =
|
|
|
709
709
|
| 'step-start' | 'step-complete' | 'validation-run'
|
|
710
710
|
| 'fix-attempt' | 'git-operation' | 'notification-sent'
|
|
711
711
|
| 'run-complete'
|
|
712
|
-
| 'ai-request' | 'ai-response' | 'tool-call' | 'tool-result'
|
|
712
|
+
| 'ai-request' | 'ai-response' | 'tool-call' | 'tool-result'
|
|
713
|
+
| 'tool-rejected' | 'tool-schema-missing'
|
|
714
|
+
| 'bridge-created' | 'bridge-sethandlers' | 'bridge-tool-filtered' | 'bridge-tool-passthrough'
|
|
715
|
+
| 'bridge-preflight-ok' | 'bridge-preflight-fail';
|
|
713
716
|
|
|
714
717
|
export interface AuditEvent {
|
|
715
718
|
type: AuditEventType;
|
package/src/bot/weaver-tools.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { execFileSync } from 'node:child_process';
|
|
10
10
|
import { executeStep } from './step-executor.js';
|
|
11
|
-
import type
|
|
11
|
+
import { stripMcpToolPrefix, type ToolDefinition } from '@synergenius/flow-weaver/agent';
|
|
12
12
|
import { BOT_TOOLS as WEAVER_TOOLS } from './tool-registry.js';
|
|
13
13
|
import { isBlockedUrl } from './safety.js';
|
|
14
14
|
|
|
@@ -37,7 +37,7 @@ export function createWeaverExecutor(projectDir: string) {
|
|
|
37
37
|
// The Claude CLI registers MCP tools as `mcp__<server>__<tool>` and the
|
|
38
38
|
// API streams back tool_use events with the prefixed name. The agent loop
|
|
39
39
|
// passes this name to the executor, but our switch cases use unprefixed names.
|
|
40
|
-
const name = rawName
|
|
40
|
+
const name = stripMcpToolPrefix(rawName);
|
|
41
41
|
|
|
42
42
|
// Handle new tools that bypass step-executor
|
|
43
43
|
switch (name) {
|
|
@@ -102,50 +102,12 @@ export function createWeaverExecutor(projectDir: string) {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
case 'task_create': {
|
|
105
|
-
const {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// Idempotent: skip if a task with the same title+parent already exists.
|
|
111
|
-
// Prevents orchestrator retries from creating duplicate subtasks.
|
|
112
|
-
if (parentId) {
|
|
113
|
-
const all = await store.list();
|
|
114
|
-
const existing = all.find(
|
|
115
|
-
t => t.parentId === parentId && t.title.toLowerCase() === title.toLowerCase(),
|
|
116
|
-
);
|
|
117
|
-
if (existing) {
|
|
118
|
-
return { result: JSON.stringify({ id: existing.id, title: existing.title, status: existing.status, skipped: true }), isError: false };
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Parse acceptance criteria if provided
|
|
123
|
-
const rawAcc = args.acceptance as Record<string, unknown> | undefined;
|
|
124
|
-
const acceptance = rawAcc?.checks ? {
|
|
125
|
-
checks: (rawAcc.checks as Array<{ name: string; command: string }>).map(c => ({
|
|
126
|
-
name: String(c.name ?? ''),
|
|
127
|
-
command: String(c.command ?? ''),
|
|
128
|
-
})).filter(c => c.name && c.command),
|
|
129
|
-
} : undefined;
|
|
130
|
-
|
|
131
|
-
const task = await store.create({
|
|
132
|
-
title,
|
|
133
|
-
description: String(args.description ?? ''),
|
|
134
|
-
priority: (args.priority as number) ?? 0,
|
|
135
|
-
parentId,
|
|
136
|
-
dependsOn: (args.dependsOn as string[]) ?? [],
|
|
137
|
-
assignedProfile: args.assignedProfile as string | undefined,
|
|
138
|
-
complexity: args.complexity as 'trivial' | 'simple' | 'moderate' | 'complex' | undefined,
|
|
139
|
-
acceptance,
|
|
140
|
-
createdBy: 'ai',
|
|
141
|
-
});
|
|
142
|
-
// Set files if provided
|
|
143
|
-
if (args.files && Array.isArray(args.files)) {
|
|
144
|
-
await store.update(task.id, {
|
|
145
|
-
context: { ...task.context, files: args.files as string[] },
|
|
146
|
-
});
|
|
105
|
+
const { handleTaskCreate } = await import('./task-create-handler.js');
|
|
106
|
+
const result = await handleTaskCreate(args as any, projectDir);
|
|
107
|
+
if (result.blocked) {
|
|
108
|
+
return { result: result.blockReason ?? 'Blocked', isError: true };
|
|
147
109
|
}
|
|
148
|
-
return { result:
|
|
110
|
+
return { result: result.output, isError: false };
|
|
149
111
|
}
|
|
150
112
|
|
|
151
113
|
case 'task_list': {
|