@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.
Files changed (181) hide show
  1. package/dist/ai-chat-provider.js +5 -5
  2. package/dist/ai-chat-provider.js.map +1 -1
  3. package/dist/bot/acceptance-merge.d.ts +21 -0
  4. package/dist/bot/acceptance-merge.d.ts.map +1 -0
  5. package/dist/bot/acceptance-merge.js +46 -0
  6. package/dist/bot/acceptance-merge.js.map +1 -0
  7. package/dist/bot/ai-client.d.ts +14 -2
  8. package/dist/bot/ai-client.d.ts.map +1 -1
  9. package/dist/bot/ai-client.js +71 -24
  10. package/dist/bot/ai-client.js.map +1 -1
  11. package/dist/bot/assistant-tools.js +3 -3
  12. package/dist/bot/assistant-tools.js.map +1 -1
  13. package/dist/bot/audit-logger.d.ts.map +1 -1
  14. package/dist/bot/audit-logger.js +34 -14
  15. package/dist/bot/audit-logger.js.map +1 -1
  16. package/dist/bot/audit-trail.d.ts +67 -0
  17. package/dist/bot/audit-trail.d.ts.map +1 -0
  18. package/dist/bot/audit-trail.js +153 -0
  19. package/dist/bot/audit-trail.js.map +1 -0
  20. package/dist/bot/behavior-defaults.d.ts +1 -1
  21. package/dist/bot/behavior-defaults.d.ts.map +1 -1
  22. package/dist/bot/behavior-defaults.js +7 -3
  23. package/dist/bot/behavior-defaults.js.map +1 -1
  24. package/dist/bot/capability-registry.d.ts +9 -0
  25. package/dist/bot/capability-registry.d.ts.map +1 -1
  26. package/dist/bot/capability-registry.js +81 -27
  27. package/dist/bot/capability-registry.js.map +1 -1
  28. package/dist/bot/capability-types.d.ts +10 -0
  29. package/dist/bot/capability-types.d.ts.map +1 -1
  30. package/dist/bot/cli-provider.d.ts.map +1 -1
  31. package/dist/bot/cli-provider.js +8 -7
  32. package/dist/bot/cli-provider.js.map +1 -1
  33. package/dist/bot/preflight.d.ts +48 -0
  34. package/dist/bot/preflight.d.ts.map +1 -0
  35. package/dist/bot/preflight.js +247 -0
  36. package/dist/bot/preflight.js.map +1 -0
  37. package/dist/bot/provider-shim.d.ts +74 -0
  38. package/dist/bot/provider-shim.d.ts.map +1 -0
  39. package/dist/bot/provider-shim.js +176 -0
  40. package/dist/bot/provider-shim.js.map +1 -0
  41. package/dist/bot/runner.d.ts +2 -0
  42. package/dist/bot/runner.d.ts.map +1 -1
  43. package/dist/bot/runner.js +60 -17
  44. package/dist/bot/runner.js.map +1 -1
  45. package/dist/bot/step-executor.d.ts.map +1 -1
  46. package/dist/bot/step-executor.js +72 -115
  47. package/dist/bot/step-executor.js.map +1 -1
  48. package/dist/bot/swarm-controller.d.ts +2 -0
  49. package/dist/bot/swarm-controller.d.ts.map +1 -1
  50. package/dist/bot/swarm-controller.js +92 -20
  51. package/dist/bot/swarm-controller.js.map +1 -1
  52. package/dist/bot/task-create-handler.d.ts +37 -0
  53. package/dist/bot/task-create-handler.d.ts.map +1 -0
  54. package/dist/bot/task-create-handler.js +124 -0
  55. package/dist/bot/task-create-handler.js.map +1 -0
  56. package/dist/bot/task-store.d.ts +1 -0
  57. package/dist/bot/task-store.d.ts.map +1 -1
  58. package/dist/bot/task-store.js +67 -0
  59. package/dist/bot/task-store.js.map +1 -1
  60. package/dist/bot/types.d.ts +1 -1
  61. package/dist/bot/types.d.ts.map +1 -1
  62. package/dist/bot/weaver-tools.d.ts.map +1 -1
  63. package/dist/bot/weaver-tools.js +7 -39
  64. package/dist/bot/weaver-tools.js.map +1 -1
  65. package/dist/node-types/agent-execute.d.ts +25 -8
  66. package/dist/node-types/agent-execute.d.ts.map +1 -1
  67. package/dist/node-types/agent-execute.js +89 -23
  68. package/dist/node-types/agent-execute.js.map +1 -1
  69. package/dist/node-types/bot-report.d.ts.map +1 -1
  70. package/dist/node-types/bot-report.js +24 -3
  71. package/dist/node-types/bot-report.js.map +1 -1
  72. package/dist/node-types/plan-task.d.ts +8 -17
  73. package/dist/node-types/plan-task.d.ts.map +1 -1
  74. package/dist/node-types/plan-task.js +217 -256
  75. package/dist/node-types/plan-task.js.map +1 -1
  76. package/dist/node-types/review-result.js +8 -6
  77. package/dist/node-types/review-result.js.map +1 -1
  78. package/dist/palindrome.d.ts +9 -0
  79. package/dist/palindrome.d.ts.map +1 -0
  80. package/dist/palindrome.js +14 -0
  81. package/dist/palindrome.js.map +1 -0
  82. package/dist/ui/approval-card.js +91 -82
  83. package/dist/ui/bot-activity.js +73 -56
  84. package/dist/ui/bot-config.js +48 -31
  85. package/dist/ui/bot-dashboard.js +52 -36
  86. package/dist/ui/bot-panel.js +230 -228
  87. package/dist/ui/bot-slot-card.js +100 -90
  88. package/dist/ui/bot-status.js +37 -15
  89. package/dist/ui/budget-bar.js +57 -31
  90. package/dist/ui/capability-editor.js +447 -378
  91. package/dist/ui/chat-task-result.js +78 -71
  92. package/dist/ui/decision-log.js +68 -81
  93. package/dist/ui/genesis-block.js +86 -95
  94. package/dist/ui/instance-stream-view.js +722 -0
  95. package/dist/ui/profile-card.js +96 -221
  96. package/dist/ui/profile-editor.js +532 -575
  97. package/dist/ui/settings-section.js +41 -45
  98. package/dist/ui/swarm-controls.js +212 -135
  99. package/dist/ui/swarm-dashboard.js +3992 -2715
  100. package/dist/ui/task-detail-view.js +415 -521
  101. package/dist/ui/task-editor.js +339 -390
  102. package/dist/ui/task-pool-list.js +60 -55
  103. package/dist/workflows/src/palindrome.d.ts +11 -0
  104. package/dist/workflows/src/palindrome.d.ts.map +1 -0
  105. package/dist/workflows/src/palindrome.js +16 -0
  106. package/dist/workflows/src/palindrome.js.map +1 -0
  107. package/dist/workflows/tests/palindrome.test.d.ts +2 -0
  108. package/dist/workflows/tests/palindrome.test.d.ts.map +1 -0
  109. package/dist/workflows/tests/palindrome.test.js +41 -0
  110. package/dist/workflows/tests/palindrome.test.js.map +1 -0
  111. package/dist/workflows/weaver-bot-batch.js +1 -1
  112. package/dist/workflows/weaver-bot-batch.js.map +1 -1
  113. package/dist/workflows/weaver-bot.js +1 -1
  114. package/dist/workflows/weaver-bot.js.map +1 -1
  115. package/flowweaver.manifest.json +1 -1
  116. package/package.json +8 -2
  117. package/src/ai-chat-provider.ts +5 -5
  118. package/src/bot/acceptance-merge.ts +62 -0
  119. package/src/bot/ai-client.ts +77 -21
  120. package/src/bot/assistant-tools.ts +3 -3
  121. package/src/bot/audit-logger.ts +42 -14
  122. package/src/bot/audit-trail.ts +211 -0
  123. package/src/bot/behavior-defaults.ts +7 -2
  124. package/src/bot/capability-registry.ts +84 -28
  125. package/src/bot/capability-types.ts +11 -0
  126. package/src/bot/cli-provider.ts +8 -7
  127. package/src/bot/preflight.ts +285 -0
  128. package/src/bot/provider-shim.ts +218 -0
  129. package/src/bot/runner.ts +68 -20
  130. package/src/bot/step-executor.ts +69 -127
  131. package/src/bot/swarm-controller.ts +94 -20
  132. package/src/bot/task-create-handler.ts +164 -0
  133. package/src/bot/task-store.ts +83 -0
  134. package/src/bot/types.ts +4 -1
  135. package/src/bot/weaver-tools.ts +7 -45
  136. package/src/node-types/agent-execute.ts +102 -16
  137. package/src/node-types/bot-report.ts +24 -3
  138. package/src/node-types/plan-task.ts +238 -280
  139. package/src/node-types/review-result.ts +8 -6
  140. package/src/palindrome.ts +14 -0
  141. package/src/ui/approval-card.tsx +78 -62
  142. package/src/ui/bot-activity.tsx +12 -10
  143. package/src/ui/bot-config.tsx +12 -10
  144. package/src/ui/bot-dashboard.tsx +13 -11
  145. package/src/ui/bot-panel.tsx +189 -171
  146. package/src/ui/bot-slot-card.tsx +125 -70
  147. package/src/ui/bot-status.tsx +4 -4
  148. package/src/ui/budget-bar.tsx +86 -25
  149. package/src/ui/capability-editor.tsx +392 -257
  150. package/src/ui/chat-task-result.tsx +81 -78
  151. package/src/ui/decision-log.tsx +76 -73
  152. package/src/ui/genesis-block.tsx +91 -61
  153. package/src/ui/instance-stream-view.tsx +861 -0
  154. package/src/ui/profile-card.tsx +195 -168
  155. package/src/ui/profile-editor.tsx +453 -370
  156. package/src/ui/settings-section.tsx +46 -39
  157. package/src/ui/swarm-controls.tsx +252 -123
  158. package/src/ui/swarm-dashboard.tsx +999 -466
  159. package/src/ui/task-detail-view.tsx +485 -428
  160. package/src/ui/task-editor.tsx +329 -271
  161. package/src/ui/task-pool-list.tsx +68 -62
  162. package/src/workflows/src/palindrome.ts +16 -0
  163. package/src/workflows/tests/palindrome.test.ts +49 -0
  164. package/src/workflows/weaver-bot-batch.ts +1 -1
  165. package/src/workflows/weaver-bot.ts +1 -1
  166. package/dist/ui/bot-constants.d.ts +0 -14
  167. package/dist/ui/bot-constants.d.ts.map +0 -1
  168. package/dist/ui/bot-constants.js +0 -189
  169. package/dist/ui/bot-constants.js.map +0 -1
  170. package/dist/ui/steer-api.d.ts +0 -7
  171. package/dist/ui/steer-api.d.ts.map +0 -1
  172. package/dist/ui/steer-api.js +0 -11
  173. package/dist/ui/steer-api.js.map +0 -1
  174. package/dist/ui/trace-to-timeline.d.ts +0 -91
  175. package/dist/ui/trace-to-timeline.d.ts.map +0 -1
  176. package/dist/ui/trace-to-timeline.js +0 -116
  177. package/dist/ui/trace-to-timeline.js.map +0 -1
  178. package/dist/ui/use-stream-timeline.d.ts +0 -50
  179. package/dist/ui/use-stream-timeline.d.ts.map +0 -1
  180. package/dist/ui/use-stream-timeline.js +0 -245
  181. 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 && durationMs < 10_000) {
357
- // Ran for less than 10s with no AI tokens something failed before work started
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
- const criteria = task.acceptance;
380
- if (!criteria || !criteria.checks.length) {
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 criteria.checks) {
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
- results.push({ name: check.name, pass: true });
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 detail = (e.stdout ?? e.stderr ?? e.message ?? '').slice(0, 500);
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, profileId: d.assignedProfileId, instanceId: d.assignedInstanceId, method: d.method, reason: d.reason },
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
- const workflowPath = path.isAbsolute(bot.filePath)
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: 'auto' },
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 (!task?.acceptance) {
971
- releaseStatus = 'done';
972
- } else {
1046
+ // Always run acceptance — capability checks may exist even if task has no checks
1047
+ {
973
1048
  try {
974
- // Run deterministic acceptance check
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
+ }
@@ -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;
@@ -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 { ToolDefinition } from '@synergenius/flow-weaver/agent';
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.replace(/^mcp__[a-zA-Z0-9_-]+__/, '');
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 { TaskStore } = await import('./task-store.js');
106
- const store = new TaskStore(projectDir);
107
- const title = String(args.title);
108
- const parentId = args.parentId as string | undefined;
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: JSON.stringify({ id: task.id, title: task.title, status: task.status }), isError: false };
110
+ return { result: result.output, isError: false };
149
111
  }
150
112
 
151
113
  case 'task_list': {