funolio-agent 0.17.9 → 1.0.3

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 (147) hide show
  1. package/dist/approval.d.ts +7 -6
  2. package/dist/approval.d.ts.map +1 -1
  3. package/dist/approval.js +40 -10
  4. package/dist/approval.js.map +1 -1
  5. package/dist/backfill.js +2 -2
  6. package/dist/backfill.js.map +1 -1
  7. package/dist/clerk-model.d.ts +0 -1
  8. package/dist/clerk-model.d.ts.map +1 -1
  9. package/dist/clerk-model.js +24 -50
  10. package/dist/clerk-model.js.map +1 -1
  11. package/dist/commands/configure-provider.js +2 -2
  12. package/dist/commands/configure-provider.js.map +1 -1
  13. package/dist/commands/configure.d.ts.map +1 -1
  14. package/dist/commands/configure.js +10 -14
  15. package/dist/commands/configure.js.map +1 -1
  16. package/dist/commands/start.d.ts.map +1 -1
  17. package/dist/commands/start.js +17 -2
  18. package/dist/commands/start.js.map +1 -1
  19. package/dist/config.d.ts +1 -1
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +2 -2
  22. package/dist/config.js.map +1 -1
  23. package/dist/context-window.d.ts +2 -8
  24. package/dist/context-window.d.ts.map +1 -1
  25. package/dist/context-window.js +4 -4
  26. package/dist/context-window.js.map +1 -1
  27. package/dist/eval/orchestrator-front-door-replay.js +43 -2
  28. package/dist/eval/orchestrator-front-door-replay.js.map +1 -1
  29. package/dist/eval/orchestrator-todo-dispatch-replay.d.ts +2 -0
  30. package/dist/eval/orchestrator-todo-dispatch-replay.d.ts.map +1 -0
  31. package/dist/eval/orchestrator-todo-dispatch-replay.js +253 -0
  32. package/dist/eval/orchestrator-todo-dispatch-replay.js.map +1 -0
  33. package/dist/eval/orchestrator-todo-planning-replay.d.ts +2 -0
  34. package/dist/eval/orchestrator-todo-planning-replay.d.ts.map +1 -0
  35. package/dist/eval/orchestrator-todo-planning-replay.js +247 -0
  36. package/dist/eval/orchestrator-todo-planning-replay.js.map +1 -0
  37. package/dist/eval/policy-detection-replay.d.ts +2 -0
  38. package/dist/eval/policy-detection-replay.d.ts.map +1 -0
  39. package/dist/eval/policy-detection-replay.js +122 -0
  40. package/dist/eval/policy-detection-replay.js.map +1 -0
  41. package/dist/eval/todo-worker-runtime-replay.d.ts +2 -0
  42. package/dist/eval/todo-worker-runtime-replay.d.ts.map +1 -0
  43. package/dist/eval/todo-worker-runtime-replay.js +520 -0
  44. package/dist/eval/todo-worker-runtime-replay.js.map +1 -0
  45. package/dist/integration-tokens.d.ts +6 -0
  46. package/dist/integration-tokens.d.ts.map +1 -1
  47. package/dist/integration-tokens.js +43 -0
  48. package/dist/integration-tokens.js.map +1 -1
  49. package/dist/local-data.d.ts +128 -1
  50. package/dist/local-data.d.ts.map +1 -1
  51. package/dist/local-data.js +702 -18
  52. package/dist/local-data.js.map +1 -1
  53. package/dist/local-db.d.ts.map +1 -1
  54. package/dist/local-db.js +216 -12
  55. package/dist/local-db.js.map +1 -1
  56. package/dist/local-funnel.d.ts.map +1 -1
  57. package/dist/local-funnel.js +7 -0
  58. package/dist/local-funnel.js.map +1 -1
  59. package/dist/local-server.d.ts.map +1 -1
  60. package/dist/local-server.js +119 -96
  61. package/dist/local-server.js.map +1 -1
  62. package/dist/mcp/bridge-server.d.ts.map +1 -1
  63. package/dist/mcp/bridge-server.js +8 -2
  64. package/dist/mcp/bridge-server.js.map +1 -1
  65. package/dist/mcp/manager.d.ts +5 -0
  66. package/dist/mcp/manager.d.ts.map +1 -1
  67. package/dist/mcp/manager.js +36 -0
  68. package/dist/mcp/manager.js.map +1 -1
  69. package/dist/mcp/sync-cli-config.d.ts +5 -0
  70. package/dist/mcp/sync-cli-config.d.ts.map +1 -1
  71. package/dist/mcp/sync-cli-config.js +10 -2
  72. package/dist/mcp/sync-cli-config.js.map +1 -1
  73. package/dist/message-loop.d.ts.map +1 -1
  74. package/dist/message-loop.js +113 -14
  75. package/dist/message-loop.js.map +1 -1
  76. package/dist/mqtt-client.d.ts +44 -0
  77. package/dist/mqtt-client.d.ts.map +1 -1
  78. package/dist/mqtt-client.js.map +1 -1
  79. package/dist/orchestration/front-door-policy.d.ts +26 -9
  80. package/dist/orchestration/front-door-policy.d.ts.map +1 -1
  81. package/dist/orchestration/front-door-policy.js +242 -69
  82. package/dist/orchestration/front-door-policy.js.map +1 -1
  83. package/dist/orchestration/orchestrator-blocked-prompt.d.ts +18 -0
  84. package/dist/orchestration/orchestrator-blocked-prompt.d.ts.map +1 -0
  85. package/dist/orchestration/orchestrator-blocked-prompt.js +46 -0
  86. package/dist/orchestration/orchestrator-blocked-prompt.js.map +1 -0
  87. package/dist/orchestration/orchestrator-final-response-prompt.d.ts +10 -0
  88. package/dist/orchestration/orchestrator-final-response-prompt.d.ts.map +1 -0
  89. package/dist/orchestration/orchestrator-final-response-prompt.js +39 -0
  90. package/dist/orchestration/orchestrator-final-response-prompt.js.map +1 -0
  91. package/dist/orchestration/orchestrator-operating-prompt.d.ts +11 -0
  92. package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
  93. package/dist/orchestration/orchestrator-operating-prompt.js +106 -36
  94. package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
  95. package/dist/orchestration/policy-prompt.d.ts +6 -0
  96. package/dist/orchestration/policy-prompt.d.ts.map +1 -0
  97. package/dist/orchestration/policy-prompt.js +40 -0
  98. package/dist/orchestration/policy-prompt.js.map +1 -0
  99. package/dist/orchestration/worker-operating-prompt.d.ts +16 -0
  100. package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -0
  101. package/dist/orchestration/worker-operating-prompt.js +75 -0
  102. package/dist/orchestration/worker-operating-prompt.js.map +1 -0
  103. package/dist/orchestrator.d.ts +19 -0
  104. package/dist/orchestrator.d.ts.map +1 -1
  105. package/dist/orchestrator.js +614 -54
  106. package/dist/orchestrator.js.map +1 -1
  107. package/dist/policy-detection.d.ts +40 -0
  108. package/dist/policy-detection.d.ts.map +1 -0
  109. package/dist/policy-detection.js +298 -0
  110. package/dist/policy-detection.js.map +1 -0
  111. package/dist/providers/anthropic.d.ts.map +1 -1
  112. package/dist/providers/anthropic.js +28 -5
  113. package/dist/providers/anthropic.js.map +1 -1
  114. package/dist/providers/claude-cli.d.ts.map +1 -1
  115. package/dist/providers/claude-cli.js +11 -2
  116. package/dist/providers/claude-cli.js.map +1 -1
  117. package/dist/providers/codex-cli.d.ts.map +1 -1
  118. package/dist/providers/codex-cli.js +121 -2
  119. package/dist/providers/codex-cli.js.map +1 -1
  120. package/dist/providers/index.d.ts +4 -0
  121. package/dist/providers/index.d.ts.map +1 -1
  122. package/dist/providers/index.js.map +1 -1
  123. package/dist/summarization-pipeline.d.ts +1 -4
  124. package/dist/summarization-pipeline.d.ts.map +1 -1
  125. package/dist/summarization-pipeline.js +43 -56
  126. package/dist/summarization-pipeline.js.map +1 -1
  127. package/dist/tools/index.d.ts.map +1 -1
  128. package/dist/tools/index.js +2 -0
  129. package/dist/tools/index.js.map +1 -1
  130. package/dist/tools/todo-tasks.d.ts +2 -0
  131. package/dist/tools/todo-tasks.d.ts.map +1 -1
  132. package/dist/tools/todo-tasks.js +203 -6
  133. package/dist/tools/todo-tasks.js.map +1 -1
  134. package/dist/types.d.ts +2 -0
  135. package/dist/types.d.ts.map +1 -1
  136. package/dist/wizard-state.d.ts.map +1 -1
  137. package/dist/wizard-state.js +30 -8
  138. package/dist/wizard-state.js.map +1 -1
  139. package/dist/wizard-support.d.ts +2 -2
  140. package/dist/wizard-support.d.ts.map +1 -1
  141. package/dist/wizard-support.js +80 -93
  142. package/dist/wizard-support.js.map +1 -1
  143. package/dist/workflow-engine.d.ts +3 -0
  144. package/dist/workflow-engine.d.ts.map +1 -1
  145. package/dist/workflow-engine.js +111 -82
  146. package/dist/workflow-engine.js.map +1 -1
  147. package/package.json +5 -1
@@ -49,6 +49,8 @@ exports.listProviderConnections = listProviderConnections;
49
49
  exports.getProviderConnection = getProviderConnection;
50
50
  exports.findProviderConnection = findProviderConnection;
51
51
  exports.updateProviderConnection = updateProviderConnection;
52
+ exports.normalizeCliProviderConnections = normalizeCliProviderConnections;
53
+ exports.pruneUnsupportedProviderConnections = pruneUnsupportedProviderConnections;
52
54
  exports.upsertOauthProviderConnection = upsertOauthProviderConnection;
53
55
  exports.deleteProviderConnection = deleteProviderConnection;
54
56
  exports.deleteAgentProfile = deleteAgentProfile;
@@ -82,6 +84,7 @@ exports.upsertMemoryFact = upsertMemoryFact;
82
84
  exports.searchMemoryFacts = searchMemoryFacts;
83
85
  exports.listMemoryFacts = listMemoryFacts;
84
86
  exports.deleteMemoryFact = deleteMemoryFact;
87
+ exports.getUserProfileFacts = getUserProfileFacts;
85
88
  exports.upsertEntity = upsertEntity;
86
89
  exports.getEntityGraph = getEntityGraph;
87
90
  exports.upsertEntityRelationship = upsertEntityRelationship;
@@ -111,6 +114,8 @@ exports.getEffectiveOrchestrationPolicy = getEffectiveOrchestrationPolicy;
111
114
  exports.getPendingOrchestrationPolicy = getPendingOrchestrationPolicy;
112
115
  exports.setPendingOrchestrationPolicy = setPendingOrchestrationPolicy;
113
116
  exports.clearPendingOrchestrationPolicy = clearPendingOrchestrationPolicy;
117
+ exports.getPolicyDetectionEventForMessage = getPolicyDetectionEventForMessage;
118
+ exports.recordPolicyDetectionEvent = recordPolicyDetectionEvent;
114
119
  exports.getOrchestrationCheckpoint = getOrchestrationCheckpoint;
115
120
  exports.setOrchestrationCheckpoint = setOrchestrationCheckpoint;
116
121
  exports.clearOrchestrationCheckpoint = clearOrchestrationCheckpoint;
@@ -124,11 +129,16 @@ exports.addTodoTask = addTodoTask;
124
129
  exports.editTodoTask = editTodoTask;
125
130
  exports.checkTodoTask = checkTodoTask;
126
131
  exports.completeTodoTaskByUser = completeTodoTaskByUser;
132
+ exports.listTodoArtifactsForTask = listTodoArtifactsForTask;
133
+ exports.completeTodoTaskByWorker = completeTodoTaskByWorker;
134
+ exports.blockTodoTaskByWorker = blockTodoTaskByWorker;
127
135
  exports.deleteTodoTask = deleteTodoTask;
128
136
  exports.reorderTodoTask = reorderTodoTask;
129
137
  exports.listTodoAudit = listTodoAudit;
130
138
  exports.moveNullTodoTasksToUnassigned = moveNullTodoTasksToUnassigned;
131
139
  exports.getLatestActiveTodoForConversation = getLatestActiveTodoForConversation;
140
+ exports.getNextActiveTodoForConversation = getNextActiveTodoForConversation;
141
+ exports.listCompletedTodoTasksForConversation = listCompletedTodoTasksForConversation;
132
142
  exports.createProject = createProject;
133
143
  exports.upsertProject = upsertProject;
134
144
  exports.getProject = getProject;
@@ -198,6 +208,7 @@ exports.resetDerivedData = resetDerivedData;
198
208
  const local_db_1 = require("./local-db");
199
209
  const memory_text_1 = require("./memory-text");
200
210
  const memory_extraction_1 = require("./memory-extraction");
211
+ const orchestrator_blocked_prompt_1 = require("./orchestration/orchestrator-blocked-prompt");
201
212
  const crypto = __importStar(require("crypto"));
202
213
  const default_tool_profile_1 = require("./default-tool-profile");
203
214
  let _db = null;
@@ -419,6 +430,8 @@ function setOrchestratorBot(botId) {
419
430
  transaction();
420
431
  return db.prepare('SELECT * FROM agent_profile WHERE id = ?').get(botId);
421
432
  }
433
+ const CLI_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli']);
434
+ const SUPPORTED_PROVIDER_ACCESS_MODES = new Set(['api', 'cli']);
422
435
  function createProviderConnection(p) {
423
436
  const db = getDb();
424
437
  const id = newId();
@@ -482,7 +495,93 @@ function updateProviderConnection(id, fields) {
482
495
  db.prepare(`UPDATE provider_connection SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
483
496
  return db.prepare('SELECT * FROM provider_connection WHERE id = ?').get(id);
484
497
  }
498
+ function normalizeCliProviderConnections() {
499
+ const db = getDb();
500
+ const rows = db.prepare(`SELECT * FROM provider_connection WHERE provider_id IN ('claude-cli', 'codex-cli')`).all();
501
+ const updatedIds = [];
502
+ const tx = db.transaction(() => {
503
+ for (const row of rows) {
504
+ const needsNormalization = row.access_mode !== 'cli'
505
+ || row.auth_type !== 'cli'
506
+ || !!row.api_key_enc
507
+ || !!row.oauth_token
508
+ || !!row.oauth_refresh_token
509
+ || row.oauth_expires_at != null;
510
+ if (!needsNormalization)
511
+ continue;
512
+ db.prepare(`
513
+ UPDATE provider_connection
514
+ SET access_mode = 'cli',
515
+ auth_type = 'cli',
516
+ api_key_enc = NULL,
517
+ oauth_token = NULL,
518
+ oauth_refresh_token = NULL,
519
+ oauth_expires_at = NULL,
520
+ updated_at = datetime('now')
521
+ WHERE id = ?
522
+ `).run(row.id);
523
+ updatedIds.push(row.id);
524
+ }
525
+ });
526
+ tx();
527
+ return { updatedIds };
528
+ }
529
+ function pruneUnsupportedProviderConnections() {
530
+ const db = getDb();
531
+ const rows = listProviderConnections().filter((row) => !SUPPORTED_PROVIDER_ACCESS_MODES.has(row.access_mode));
532
+ if (rows.length === 0)
533
+ return { deletedIds: [], skippedIds: [] };
534
+ const referencedByBots = new Set(db.prepare(`SELECT provider_connection_id
535
+ FROM agent_profile
536
+ WHERE provider_connection_id IS NOT NULL`).all().map((row) => row.provider_connection_id));
537
+ const clerkConfig = getJsonSetting('clerk.config');
538
+ const protectedIds = new Set([
539
+ ...referencedByBots,
540
+ ...(clerkConfig?.providerConnectionId ? [clerkConfig.providerConnectionId] : []),
541
+ ]);
542
+ const deletedIds = [];
543
+ const skippedIds = [];
544
+ const tx = db.transaction(() => {
545
+ for (const row of rows) {
546
+ if (protectedIds.has(row.id)) {
547
+ skippedIds.push(row.id);
548
+ continue;
549
+ }
550
+ db.prepare('DELETE FROM provider_connection WHERE id = ?').run(row.id);
551
+ deletedIds.push(row.id);
552
+ }
553
+ });
554
+ tx();
555
+ return { deletedIds, skippedIds };
556
+ }
485
557
  function upsertOauthProviderConnection(input) {
558
+ if (CLI_PROVIDER_IDS.has(input.providerId)) {
559
+ const existing = findProviderConnection(input.providerId, 'cli')
560
+ || listProviderConnections().find((row) => row.provider_id === input.providerId);
561
+ if (existing) {
562
+ return updateProviderConnection(existing.id, {
563
+ accessMode: 'cli',
564
+ authType: 'cli',
565
+ apiKeyEnc: null,
566
+ oauthToken: null,
567
+ oauthRefreshToken: null,
568
+ oauthExpiresAt: null,
569
+ ...(input.defaultModel !== undefined ? { defaultModel: input.defaultModel } : {}),
570
+ ...(input.label !== undefined ? { label: input.label ?? undefined } : {}),
571
+ status: 'configured',
572
+ lastError: null,
573
+ });
574
+ }
575
+ return createProviderConnection({
576
+ providerId: input.providerId,
577
+ accessMode: 'cli',
578
+ authType: 'cli',
579
+ ...(input.label !== undefined ? { label: input.label ?? undefined } : {}),
580
+ ...(input.defaultModel !== undefined ? { defaultModel: input.defaultModel ?? undefined } : {}),
581
+ status: 'configured',
582
+ lastError: undefined,
583
+ });
584
+ }
486
585
  const existing = findProviderConnection(input.providerId, 'oauth')
487
586
  || listProviderConnections().find((row) => row.provider_id === input.providerId && row.auth_type === 'oauth');
488
587
  if (existing) {
@@ -1099,6 +1198,20 @@ function listMemoryFacts(opts) {
1099
1198
  function deleteMemoryFact(id) {
1100
1199
  return getDb().prepare('DELETE FROM memory_fact WHERE id = ?').run(id).changes > 0;
1101
1200
  }
1201
+ /**
1202
+ * Get user-profile facts (person entities) for injection into the system prompt.
1203
+ * Returns high-confidence facts about people (address, preferences, etc.)
1204
+ * limited to ~500 tokens worth of content.
1205
+ */
1206
+ function getUserProfileFacts(agentId, limit = 15) {
1207
+ const db = getDb();
1208
+ return db.prepare(`
1209
+ SELECT * FROM memory_fact
1210
+ WHERE agent_id = ? AND entity_type = 'person'
1211
+ ORDER BY confidence DESC, updated_at DESC
1212
+ LIMIT ?
1213
+ `).all(agentId, limit);
1214
+ }
1102
1215
  function upsertEntity(agentId, name, type) {
1103
1216
  const db = getDb();
1104
1217
  const existing = db.prepare('SELECT * FROM entity WHERE agent_id = ? AND name = ?').get(agentId, name);
@@ -1522,6 +1635,29 @@ function setPendingOrchestrationPolicy(entry) {
1522
1635
  function clearPendingOrchestrationPolicy(conversationId) {
1523
1636
  return getDb().prepare('DELETE FROM pending_orchestration_policy WHERE conversation_id = ?').run(conversationId).changes > 0;
1524
1637
  }
1638
+ function getPolicyDetectionEventForMessage(messageId) {
1639
+ return getDb().prepare('SELECT * FROM policy_detection_event WHERE message_id = ?').get(messageId);
1640
+ }
1641
+ function recordPolicyDetectionEvent(entry) {
1642
+ const db = getDb();
1643
+ const existing = getPolicyDetectionEventForMessage(entry.messageId);
1644
+ const id = existing?.id || newId();
1645
+ db.prepare(`
1646
+ INSERT INTO policy_detection_event (
1647
+ id, conversation_id, message_id, detection_source, detected_by_code,
1648
+ signal_json, status, scope, summary_text, policy_json, created_at
1649
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
1650
+ ON CONFLICT(message_id) DO UPDATE SET
1651
+ detection_source = excluded.detection_source,
1652
+ detected_by_code = excluded.detected_by_code,
1653
+ signal_json = excluded.signal_json,
1654
+ status = excluded.status,
1655
+ scope = excluded.scope,
1656
+ summary_text = excluded.summary_text,
1657
+ policy_json = excluded.policy_json
1658
+ `).run(id, entry.conversationId, entry.messageId, entry.source, entry.detectedByCode ? 1 : 0, entry.matchedSignals?.length ? JSON.stringify(entry.matchedSignals) : null, entry.status, entry.scope ?? null, entry.summaryText ?? null, entry.policy ? JSON.stringify(entry.policy) : null);
1659
+ return getPolicyDetectionEventForMessage(entry.messageId);
1660
+ }
1525
1661
  function getOrchestrationCheckpoint(conversationId) {
1526
1662
  const row = getDb().prepare('SELECT * FROM orchestration_checkpoint WHERE conversation_id = ?').get(conversationId);
1527
1663
  if (!row)
@@ -1621,6 +1757,19 @@ function parseJsonStatus(raw) {
1621
1757
  return {};
1622
1758
  }
1623
1759
  }
1760
+ function parseJsonNumberArray(raw) {
1761
+ try {
1762
+ const parsed = JSON.parse(raw);
1763
+ if (!Array.isArray(parsed))
1764
+ return [];
1765
+ return parsed
1766
+ .map((value) => Number(value))
1767
+ .filter((value) => Number.isFinite(value) && value > 0);
1768
+ }
1769
+ catch {
1770
+ return [];
1771
+ }
1772
+ }
1624
1773
  function normalizeParticipants(input) {
1625
1774
  const seen = new Set();
1626
1775
  const out = [];
@@ -1643,11 +1792,27 @@ function normalizeTodoRow(row, state) {
1643
1792
  conversation_id: row.conversation_id ? String(row.conversation_id) : null,
1644
1793
  origin: row.origin ? String(row.origin) : null,
1645
1794
  created_by_bot_id: row.created_by_bot_id ? String(row.created_by_bot_id) : null,
1795
+ owner_bot_id: row.owner_bot_id ? String(row.owner_bot_id) : null,
1796
+ owner_name: row.owner_name ? String(row.owner_name) : null,
1797
+ task_type: row.task_type ? String(row.task_type) : null,
1798
+ profile_name: row.profile_name ? String(row.profile_name) : null,
1799
+ next_worker_bot_id: row.next_worker_bot_id ? String(row.next_worker_bot_id) : null,
1800
+ next_worker_name: row.next_worker_name ? String(row.next_worker_name) : null,
1801
+ next_worker_role: row.next_worker_role ? String(row.next_worker_role) : null,
1646
1802
  title: String(row.title || ''),
1647
1803
  details: String(row.details || ''),
1804
+ task_prompt: row.task_prompt ? String(row.task_prompt) : null,
1805
+ handoff_prompt: row.handoff_prompt ? String(row.handoff_prompt) : null,
1806
+ output_summary: row.output_summary ? String(row.output_summary) : null,
1807
+ blocker_summary: row.blocker_summary ? String(row.blocker_summary) : null,
1808
+ blocker_checked: row.blocker_checked ? String(row.blocker_checked) : null,
1809
+ blocker_question: row.blocker_question ? String(row.blocker_question) : null,
1810
+ artifact_ids: parseJsonNumberArray(String(row.artifact_ids_json || '[]')),
1811
+ internal_only: Number(row.internal_only || 0),
1648
1812
  participants: parseJsonArray(String(row.participants || '[]')),
1649
1813
  status: parseJsonStatus(String(row.status || '{}')),
1650
1814
  created_order: Number(row.created_order || 0),
1815
+ position: Number(row.created_order || 0),
1651
1816
  state,
1652
1817
  created_at: String(row.created_at || ''),
1653
1818
  completed_at: row.completed_at ? String(row.completed_at) : null,
@@ -1659,6 +1824,47 @@ function normalizeTodoRow(row, state) {
1659
1824
  function isUserName(name) {
1660
1825
  return name.trim().toLowerCase() === 'user';
1661
1826
  }
1827
+ function roleClassToTaskType(roleClass) {
1828
+ const normalized = String(roleClass || '').trim().toLowerCase();
1829
+ if (!normalized)
1830
+ return null;
1831
+ if (normalized === 'code' || normalized === 'coding' || normalized === 'developer')
1832
+ return 'coding';
1833
+ if (normalized === 'qa' || normalized === 'review')
1834
+ return 'qa';
1835
+ if (normalized === 'research' || normalized === 'brainstorm' || normalized === 'planning' || normalized === 'plan')
1836
+ return 'research';
1837
+ if (normalized === 'verify' || normalized === 'verification')
1838
+ return 'verify';
1839
+ if (normalized === 'deploy' || normalized === 'deployment')
1840
+ return 'deploy';
1841
+ return normalized;
1842
+ }
1843
+ function resolveAgentByName(name) {
1844
+ const normalized = String(name || '').trim().toLowerCase();
1845
+ if (!normalized)
1846
+ return undefined;
1847
+ return listAgentProfiles().find((agent) => agent.name.trim().toLowerCase() === normalized);
1848
+ }
1849
+ function inferTaskTypeForAgent(agent, requested) {
1850
+ const explicit = String(requested || '').trim();
1851
+ if (explicit)
1852
+ return explicit;
1853
+ return roleClassToTaskType(agent?.role_class) || roleClassToTaskType(agent?.role_label) || null;
1854
+ }
1855
+ function normalizeArtifactRow(row) {
1856
+ return {
1857
+ id: Number(row.id),
1858
+ project_id: row.project_id ? String(row.project_id) : null,
1859
+ conversation_id: row.conversation_id ? String(row.conversation_id) : null,
1860
+ artifact_type: String(row.artifact_type || 'reference'),
1861
+ label: row.label ? String(row.label) : null,
1862
+ path_or_ref: String(row.path_or_ref || ''),
1863
+ metadata_json: row.metadata_json ? String(row.metadata_json) : null,
1864
+ created_by_bot_id: row.created_by_bot_id ? String(row.created_by_bot_id) : null,
1865
+ created_at: String(row.created_at || ''),
1866
+ };
1867
+ }
1662
1868
  function resolveAssignedParticipant(participants, requested) {
1663
1869
  const want = requested.trim().toLowerCase();
1664
1870
  const found = participants.find((p) => p.trim().toLowerCase() === want);
@@ -1692,16 +1898,142 @@ function moveActiveTaskToCompletedTx(db, taskId, actor, reason) {
1692
1898
  const completedAt = now();
1693
1899
  db.prepare(`
1694
1900
  INSERT INTO todo_completed (
1695
- id, project_id, conversation_id, title, details, participants, status, created_order,
1696
- success_criteria, origin, created_by_bot_id, created_at, completed_at, last_updated, version
1697
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1698
- `).run(row.id, row.project_id ?? null, row.conversation_id ?? null, row.title, row.details, row.participants, row.status, row.created_order, row.success_criteria ?? null, row.origin ?? 'user', row.created_by_bot_id ?? null, row.created_at, completedAt, completedAt, row.version);
1901
+ id, project_id, conversation_id, origin, created_by_bot_id, owner_bot_id, owner_name,
1902
+ task_type, profile_name, next_worker_bot_id, next_worker_name, next_worker_role,
1903
+ title, details, task_prompt, handoff_prompt, output_summary, blocker_summary, blocker_checked, blocker_question, artifact_ids_json,
1904
+ success_criteria, internal_only, participants, status, created_order,
1905
+ created_at, completed_at, last_updated, version
1906
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1907
+ `).run(row.id, row.project_id ?? null, row.conversation_id ?? null, row.origin ?? 'user', row.created_by_bot_id ?? null, row.owner_bot_id ?? null, row.owner_name ?? null, row.task_type ?? null, row.profile_name ?? 'Programming', row.next_worker_bot_id ?? null, row.next_worker_name ?? null, row.next_worker_role ?? null, row.title, row.details, row.task_prompt ?? null, row.handoff_prompt ?? null, row.output_summary ?? null, row.blocker_summary ?? null, row.blocker_checked ?? null, row.blocker_question ?? null, row.artifact_ids_json ?? '[]', row.success_criteria ?? null, Number(row.internal_only || 0), row.participants, row.status, row.created_order, row.created_at, completedAt, completedAt, row.version);
1699
1908
  db.prepare('DELETE FROM todo_active WHERE id = ?').run(taskId);
1700
- normalizeTodoOrderTx(db, row.project_id ? String(row.project_id) : null);
1701
1909
  logTodoAuditTx(db, taskId, 'completed', 'complete', actor, { reason });
1702
1910
  const completed = db.prepare('SELECT * FROM todo_completed WHERE id = ?').get(taskId);
1703
1911
  return normalizeTodoRow(completed, 'completed');
1704
1912
  }
1913
+ function determineInsertPositionTx(db, projectId, insertAfterTaskId) {
1914
+ if (insertAfterTaskId) {
1915
+ const row = db.prepare(`
1916
+ SELECT created_order, project_id
1917
+ FROM todo_active
1918
+ WHERE id = ?
1919
+ UNION ALL
1920
+ SELECT created_order, project_id
1921
+ FROM todo_completed
1922
+ WHERE id = ?
1923
+ LIMIT 1
1924
+ `).get(insertAfterTaskId, insertAfterTaskId);
1925
+ if (!row)
1926
+ throw new Error(`Task ${insertAfterTaskId} not found`);
1927
+ const normalizedProjectId = projectId ?? null;
1928
+ if ((row.project_id ?? null) !== normalizedProjectId) {
1929
+ throw new Error('Inserted task must stay within the same project scope');
1930
+ }
1931
+ const targetPosition = Number(row.created_order || 0) + 1;
1932
+ if (normalizedProjectId) {
1933
+ db.prepare('UPDATE todo_active SET created_order = created_order + 1, last_updated = datetime(\'now\'), version = version + 1 WHERE project_id = ? AND created_order >= ?')
1934
+ .run(normalizedProjectId, targetPosition);
1935
+ }
1936
+ else {
1937
+ db.prepare('UPDATE todo_active SET created_order = created_order + 1, last_updated = datetime(\'now\'), version = version + 1 WHERE project_id IS NULL AND created_order >= ?')
1938
+ .run(targetPosition);
1939
+ }
1940
+ return targetPosition;
1941
+ }
1942
+ const maxOrder = projectId
1943
+ ? db.prepare('SELECT COALESCE(MAX(created_order), 0) as n FROM todo_active WHERE project_id = ?').get(projectId)
1944
+ : db.prepare('SELECT COALESCE(MAX(created_order), 0) as n FROM todo_active WHERE project_id IS NULL').get();
1945
+ return Number(maxOrder.n) + 1;
1946
+ }
1947
+ function resolveOrchestratorName() {
1948
+ return getOrchestratorBot()?.name || null;
1949
+ }
1950
+ function resolveAllowedWorkerNamesForInsertion(currentTask) {
1951
+ const allowed = new Set();
1952
+ if (currentTask.next_worker_name)
1953
+ allowed.add(currentTask.next_worker_name.trim().toLowerCase());
1954
+ const orchestratorName = resolveOrchestratorName();
1955
+ if (orchestratorName)
1956
+ allowed.add(orchestratorName.trim().toLowerCase());
1957
+ const db = getDb();
1958
+ const previous = currentTask.project_id
1959
+ ? db.prepare(`
1960
+ SELECT owner_name, created_order, internal_only
1961
+ FROM (
1962
+ SELECT owner_name, created_order, internal_only FROM todo_active WHERE project_id = ?
1963
+ UNION ALL
1964
+ SELECT owner_name, created_order, internal_only FROM todo_completed WHERE project_id = ?
1965
+ )
1966
+ WHERE created_order < ? AND COALESCE(internal_only, 0) = 0 AND owner_name IS NOT NULL
1967
+ ORDER BY created_order DESC
1968
+ LIMIT 1
1969
+ `).get(currentTask.project_id, currentTask.project_id, currentTask.position)
1970
+ : db.prepare(`
1971
+ SELECT owner_name, created_order, internal_only
1972
+ FROM (
1973
+ SELECT owner_name, created_order, internal_only FROM todo_active WHERE project_id IS NULL
1974
+ UNION ALL
1975
+ SELECT owner_name, created_order, internal_only FROM todo_completed WHERE project_id IS NULL
1976
+ )
1977
+ WHERE created_order < ? AND COALESCE(internal_only, 0) = 0 AND owner_name IS NOT NULL
1978
+ ORDER BY created_order DESC
1979
+ LIMIT 1
1980
+ `).get(currentTask.position);
1981
+ if (previous?.owner_name)
1982
+ allowed.add(previous.owner_name.trim().toLowerCase());
1983
+ return Array.from(allowed);
1984
+ }
1985
+ function normalizeArtifactInput(ref) {
1986
+ if (typeof ref === 'string') {
1987
+ const trimmed = ref.trim();
1988
+ return {
1989
+ pathOrRef: trimmed,
1990
+ artifactType: /\.[a-z0-9]+$/i.test(trimmed) || /[\\/]/.test(trimmed) ? 'file' : 'reference',
1991
+ label: trimmed.split(/[\\/]/).filter(Boolean).pop() || trimmed,
1992
+ };
1993
+ }
1994
+ return {
1995
+ artifactType: String(ref.artifactType || 'reference'),
1996
+ label: ref.label ? String(ref.label) : undefined,
1997
+ pathOrRef: String(ref.pathOrRef || '').trim(),
1998
+ metadata: ref.metadata ?? null,
1999
+ };
2000
+ }
2001
+ function resolveArtifactRowsTx(db, refs, context) {
2002
+ const rows = [];
2003
+ for (const raw of refs) {
2004
+ const normalized = normalizeArtifactInput(raw);
2005
+ if (!normalized.pathOrRef)
2006
+ continue;
2007
+ const existing = db.prepare(`
2008
+ SELECT * FROM todo_artifact
2009
+ WHERE project_id IS ? AND conversation_id IS ? AND artifact_type = ? AND path_or_ref = ?
2010
+ ORDER BY id DESC
2011
+ LIMIT 1
2012
+ `).get(context.projectId ?? null, context.conversationId ?? null, normalized.artifactType || 'reference', normalized.pathOrRef);
2013
+ if (existing) {
2014
+ rows.push(normalizeArtifactRow(existing));
2015
+ continue;
2016
+ }
2017
+ const result = db.prepare(`
2018
+ INSERT INTO todo_artifact (
2019
+ project_id, conversation_id, artifact_type, label, path_or_ref, metadata_json, created_by_bot_id, created_at
2020
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
2021
+ `).run(context.projectId ?? null, context.conversationId ?? null, normalized.artifactType || 'reference', normalized.label ?? null, normalized.pathOrRef, normalized.metadata ? JSON.stringify(normalized.metadata) : null, context.createdByBotId ?? null, now());
2022
+ const created = db.prepare('SELECT * FROM todo_artifact WHERE id = ?').get(Number(result.lastInsertRowid));
2023
+ rows.push(normalizeArtifactRow(created));
2024
+ }
2025
+ return rows;
2026
+ }
2027
+ function loadArtifactRowsByIdsTx(db, artifactIds) {
2028
+ const normalized = artifactIds
2029
+ .map((value) => Number(value))
2030
+ .filter((value) => Number.isFinite(value) && value > 0);
2031
+ if (normalized.length === 0)
2032
+ return [];
2033
+ const placeholders = normalized.map(() => '?').join(', ');
2034
+ const rows = db.prepare(`SELECT * FROM todo_artifact WHERE id IN (${placeholders}) ORDER BY id ASC`).all(...normalized);
2035
+ return rows.map((row) => normalizeArtifactRow(row));
2036
+ }
1705
2037
  function getTodoStatusMarker(projectId) {
1706
2038
  const row = projectId
1707
2039
  ? getDb().prepare('SELECT COUNT(*) as n FROM todo_active WHERE project_id = ?').get(projectId)
@@ -1736,13 +2068,24 @@ function listAllTodoTasks(opts) {
1736
2068
  };
1737
2069
  }
1738
2070
  function addTodoTask(params) {
1739
- const db = getDb();
1740
2071
  const title = String(params.title || '').trim();
1741
2072
  const details = String(params.details || '').trim();
1742
2073
  const participants = normalizeParticipants(params.participants || []);
1743
2074
  const successCriteria = params.successCriteria?.trim() || null;
1744
2075
  const origin = params.actor.actorType;
1745
2076
  const createdByBotId = params.actor.actorId?.trim() || null;
2077
+ const ownerBotId = params.ownerBotId?.trim() || null;
2078
+ const ownerName = params.ownerName?.trim() || null;
2079
+ const taskType = params.taskType?.trim() || null;
2080
+ const profileName = params.profileName?.trim() || 'Programming';
2081
+ const nextWorkerBotId = params.nextWorkerBotId?.trim() || null;
2082
+ const nextWorkerName = params.nextWorkerName?.trim() || null;
2083
+ const nextWorkerRole = params.nextWorkerRole?.trim() || null;
2084
+ const taskPrompt = params.taskPrompt?.trim() || null;
2085
+ const handoffPrompt = params.handoffPrompt?.trim() || null;
2086
+ const outputSummary = params.outputSummary?.trim() || null;
2087
+ const artifactIdsJson = JSON.stringify((params.artifactIds || []).filter((id) => Number.isFinite(id) && id > 0));
2088
+ const internalOnly = params.internalOnly ? 1 : 0;
1746
2089
  if (!title)
1747
2090
  throw new Error('Title is required');
1748
2091
  if (countWords(title) > TODO_TITLE_WORD_LIMIT)
@@ -1757,21 +2100,61 @@ function addTodoTask(params) {
1757
2100
  if (!successCriteria)
1758
2101
  throw new Error('Orchestrator TODOs must include success criteria');
1759
2102
  }
1760
- const tx = db.transaction(() => {
1761
- const maxOrder = params.projectId
1762
- ? db.prepare('SELECT COALESCE(MAX(created_order), 0) as n FROM todo_active WHERE project_id = ?').get(params.projectId)
1763
- : db.prepare('SELECT COALESCE(MAX(created_order), 0) as n FROM todo_active WHERE project_id IS NULL').get();
1764
- const createdOrder = Number(maxOrder.n) + 1;
2103
+ const db = getDb();
2104
+ const tx = db.transaction(() => insertTodoTaskTx(db, {
2105
+ ...params,
2106
+ title,
2107
+ details,
2108
+ participants,
2109
+ successCriteria: successCriteria || undefined,
2110
+ ownerBotId,
2111
+ ownerName,
2112
+ taskType,
2113
+ profileName,
2114
+ nextWorkerBotId,
2115
+ nextWorkerName,
2116
+ nextWorkerRole,
2117
+ taskPrompt,
2118
+ handoffPrompt,
2119
+ outputSummary,
2120
+ artifactIds: JSON.parse(artifactIdsJson),
2121
+ internalOnly: Boolean(internalOnly),
2122
+ }));
2123
+ return tx();
2124
+ }
2125
+ function insertTodoTaskTx(db, params) {
2126
+ const title = String(params.title || '').trim();
2127
+ const details = String(params.details || '').trim();
2128
+ const participants = normalizeParticipants(params.participants || []);
2129
+ const successCriteria = params.successCriteria?.trim() || null;
2130
+ const origin = params.actor.actorType;
2131
+ const createdByBotId = params.actor.actorId?.trim() || null;
2132
+ const ownerBotId = params.ownerBotId?.trim() || null;
2133
+ const ownerName = params.ownerName?.trim() || null;
2134
+ const taskType = params.taskType?.trim() || null;
2135
+ const profileName = params.profileName?.trim() || 'Programming';
2136
+ const nextWorkerBotId = params.nextWorkerBotId?.trim() || null;
2137
+ const nextWorkerName = params.nextWorkerName?.trim() || null;
2138
+ const nextWorkerRole = params.nextWorkerRole?.trim() || null;
2139
+ const taskPrompt = params.taskPrompt?.trim() || null;
2140
+ const handoffPrompt = params.handoffPrompt?.trim() || null;
2141
+ const outputSummary = params.outputSummary?.trim() || null;
2142
+ const artifactIdsJson = JSON.stringify((params.artifactIds || []).filter((id) => Number.isFinite(id) && id > 0));
2143
+ const internalOnly = params.internalOnly ? 1 : 0;
2144
+ {
2145
+ const createdOrder = determineInsertPositionTx(db, params.projectId, params.insertAfterTaskId);
1765
2146
  const status = {};
1766
2147
  for (const p of participants)
1767
2148
  status[p] = false;
1768
2149
  const result = db.prepare(`
1769
2150
  INSERT INTO todo_active (
1770
- project_id, conversation_id, origin, created_by_bot_id, title, details,
1771
- participants, status, created_order, success_criteria, created_at, last_updated, version
2151
+ project_id, conversation_id, origin, created_by_bot_id, owner_bot_id, owner_name,
2152
+ task_type, profile_name, next_worker_bot_id, next_worker_name, next_worker_role,
2153
+ title, details, task_prompt, handoff_prompt, output_summary, artifact_ids_json,
2154
+ success_criteria, internal_only, participants, status, created_order, created_at, last_updated, version
1772
2155
  )
1773
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
1774
- `).run(params.projectId ?? null, params.conversationId ?? null, origin, createdByBotId, title, details, JSON.stringify(participants), JSON.stringify(status), createdOrder, successCriteria, now(), now());
2156
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
2157
+ `).run(params.projectId ?? null, params.conversationId ?? null, origin, createdByBotId, ownerBotId, ownerName, taskType, profileName, nextWorkerBotId, nextWorkerName, nextWorkerRole, title, details, taskPrompt, handoffPrompt, outputSummary, artifactIdsJson, successCriteria, internalOnly, JSON.stringify(participants), JSON.stringify(status), createdOrder, now(), now());
1775
2158
  const id = Number(result.lastInsertRowid);
1776
2159
  logTodoAuditTx(db, id, 'active', 'add', params.actor, {
1777
2160
  title,
@@ -1780,11 +2163,19 @@ function addTodoTask(params) {
1780
2163
  conversationId: params.conversationId ?? null,
1781
2164
  origin,
1782
2165
  createdByBotId,
2166
+ ownerBotId,
2167
+ ownerName,
2168
+ taskType,
2169
+ profileName,
2170
+ nextWorkerBotId,
2171
+ nextWorkerName,
2172
+ nextWorkerRole,
2173
+ insertAfterTaskId: params.insertAfterTaskId ?? null,
2174
+ internalOnly,
1783
2175
  });
1784
2176
  const row = db.prepare('SELECT * FROM todo_active WHERE id = ?').get(id);
1785
2177
  return normalizeTodoRow(row, 'active');
1786
- });
1787
- return tx();
2178
+ }
1788
2179
  }
1789
2180
  function editTodoTask(taskId, params) {
1790
2181
  const db = getDb();
@@ -1920,6 +2311,289 @@ function completeTodoTaskByUser(taskId, params) {
1920
2311
  });
1921
2312
  return tx();
1922
2313
  }
2314
+ function listTodoArtifactsForTask(taskId, state = 'active') {
2315
+ const task = getTodoTask(taskId, state);
2316
+ if (!task || task.artifact_ids.length === 0)
2317
+ return [];
2318
+ const placeholders = task.artifact_ids.map(() => '?').join(', ');
2319
+ const rows = getDb().prepare(`SELECT * FROM todo_artifact WHERE id IN (${placeholders}) ORDER BY id ASC`).all(...task.artifact_ids);
2320
+ return rows.map((row) => normalizeArtifactRow(row));
2321
+ }
2322
+ function completeTodoTaskByWorker(taskId, params) {
2323
+ if (params.actor.actorType !== 'llm' && params.actor.actorType !== 'orchestrator') {
2324
+ throw new Error('Only worker LLMs or orchestrator can complete workflow TODOs');
2325
+ }
2326
+ const db = getDb();
2327
+ const tx = db.transaction(() => {
2328
+ const row = db.prepare('SELECT * FROM todo_active WHERE id = ?').get(taskId);
2329
+ if (!row)
2330
+ throw new Error(`Task ${taskId} not found`);
2331
+ if (params.expectedVersion !== undefined && Number(row.version) !== Number(params.expectedVersion)) {
2332
+ throw new Error('Conflict: task version changed');
2333
+ }
2334
+ if (params.expectedLastUpdated !== undefined && String(row.last_updated) !== String(params.expectedLastUpdated)) {
2335
+ throw new Error('Conflict: task was updated by another actor');
2336
+ }
2337
+ const currentTask = normalizeTodoRow(row, 'active');
2338
+ const actorName = String(params.actor.actorId || '').trim();
2339
+ const actorBot = resolveAgentByName(actorName);
2340
+ const isAssignedWorker = Boolean(actorName &&
2341
+ currentTask.owner_name &&
2342
+ actorName.toLowerCase() === currentTask.owner_name.toLowerCase());
2343
+ const isOrchestratorActor = params.actor.actorType === 'orchestrator';
2344
+ if (!isAssignedWorker && !isOrchestratorActor) {
2345
+ throw new Error(`Actor "${actorName || 'unknown'}" is not the owner of task ${taskId}`);
2346
+ }
2347
+ const existingArtifactRows = loadArtifactRowsByIdsTx(db, currentTask.artifact_ids);
2348
+ const newArtifactRows = resolveArtifactRowsTx(db, params.artifactRefs || [], {
2349
+ projectId: currentTask.project_id,
2350
+ conversationId: currentTask.conversation_id,
2351
+ createdByBotId: actorBot?.id || currentTask.owner_bot_id || currentTask.created_by_bot_id,
2352
+ });
2353
+ const artifactRowsById = new Map();
2354
+ for (const item of [...existingArtifactRows, ...newArtifactRows])
2355
+ artifactRowsById.set(item.id, item);
2356
+ const artifactRows = Array.from(artifactRowsById.values()).sort((a, b) => a.id - b.id);
2357
+ const artifactIds = artifactRows.map((item) => item.id);
2358
+ const outputSummary = params.outputSummary?.trim() || currentTask.output_summary || null;
2359
+ const handoffPrompt = params.handoffPrompt?.trim() || currentTask.handoff_prompt || null;
2360
+ db.prepare(`
2361
+ UPDATE todo_active
2362
+ SET output_summary = ?,
2363
+ handoff_prompt = ?,
2364
+ artifact_ids_json = ?,
2365
+ last_updated = ?,
2366
+ version = version + 1
2367
+ WHERE id = ?
2368
+ `).run(outputSummary, handoffPrompt, JSON.stringify(artifactIds), now(), taskId);
2369
+ logTodoAuditTx(db, taskId, 'active', 'worker_complete', params.actor, {
2370
+ outputSummary,
2371
+ artifactIds,
2372
+ inserted: Boolean(params.insertTask),
2373
+ });
2374
+ const completedTask = moveActiveTaskToCompletedTx(db, taskId, params.actor, 'all_llms_complete');
2375
+ let insertedTask;
2376
+ let handedOffTask;
2377
+ let returnedToOrchestrator = false;
2378
+ if (params.insertTask) {
2379
+ const allowedTargets = resolveAllowedWorkerNamesForInsertion(completedTask);
2380
+ const requestedWorkerName = String(params.insertTask.nextWorker || '').trim();
2381
+ if (!requestedWorkerName) {
2382
+ throw new Error('Inserted worker task requires a next worker');
2383
+ }
2384
+ if (!allowedTargets.includes(requestedWorkerName.toLowerCase())) {
2385
+ throw new Error(`Next worker "${requestedWorkerName}" is not allowed from this task`);
2386
+ }
2387
+ const targetWorker = resolveAgentByName(requestedWorkerName);
2388
+ const orchestratorName = resolveOrchestratorName();
2389
+ if (!targetWorker && (!orchestratorName || requestedWorkerName.toLowerCase() !== orchestratorName.toLowerCase())) {
2390
+ throw new Error(`Unknown next worker "${requestedWorkerName}"`);
2391
+ }
2392
+ if (orchestratorName && requestedWorkerName.toLowerCase() === orchestratorName.toLowerCase()) {
2393
+ returnedToOrchestrator = true;
2394
+ logTodoAuditTx(db, taskId, 'completed', 'return_to_orchestrator', params.actor, {
2395
+ handoffPrompt: params.insertTask.prompt,
2396
+ title: params.insertTask.title,
2397
+ });
2398
+ }
2399
+ else {
2400
+ const participants = targetWorker?.name ? [targetWorker.name] : [requestedWorkerName];
2401
+ insertedTask = insertTodoTaskTx(db, {
2402
+ projectId: completedTask.project_id,
2403
+ conversationId: completedTask.conversation_id,
2404
+ title: params.insertTask.title,
2405
+ details: params.insertTask.details || params.insertTask.prompt,
2406
+ participants,
2407
+ successCriteria: params.insertTask.successCriteria || undefined,
2408
+ ownerBotId: targetWorker?.id || null,
2409
+ ownerName: targetWorker?.name || requestedWorkerName,
2410
+ taskType: inferTaskTypeForAgent(targetWorker, params.insertTask.taskType),
2411
+ profileName: completedTask.profile_name || 'Programming',
2412
+ nextWorkerBotId: currentTask.owner_bot_id,
2413
+ nextWorkerName: currentTask.owner_name,
2414
+ nextWorkerRole: actorBot?.role_label || actorBot?.role_class || null,
2415
+ taskPrompt: params.insertTask.prompt,
2416
+ artifactIds,
2417
+ insertAfterTaskId: completedTask.id,
2418
+ actor: params.actor,
2419
+ });
2420
+ logTodoAuditTx(db, insertedTask.id, 'active', 'worker_insert', params.actor, {
2421
+ insertedAfterTaskId: completedTask.id,
2422
+ requestedWorkerName,
2423
+ });
2424
+ }
2425
+ }
2426
+ else {
2427
+ const nextWorkerName = completedTask.next_worker_name?.trim();
2428
+ if (nextWorkerName) {
2429
+ const nextRow = completedTask.next_worker_bot_id
2430
+ ? db.prepare(`
2431
+ SELECT * FROM todo_active
2432
+ WHERE conversation_id = ?
2433
+ AND id <> ?
2434
+ AND owner_bot_id = ?
2435
+ AND created_order > ?
2436
+ AND COALESCE(internal_only, 0) = 0
2437
+ ORDER BY created_order ASC, id ASC
2438
+ LIMIT 1
2439
+ `).get(completedTask.conversation_id, completedTask.id, completedTask.next_worker_bot_id, completedTask.created_order)
2440
+ : db.prepare(`
2441
+ SELECT * FROM todo_active
2442
+ WHERE conversation_id = ?
2443
+ AND id <> ?
2444
+ AND LOWER(COALESCE(owner_name, '')) = LOWER(?)
2445
+ AND created_order > ?
2446
+ AND COALESCE(internal_only, 0) = 0
2447
+ ORDER BY created_order ASC, id ASC
2448
+ LIMIT 1
2449
+ `).get(completedTask.conversation_id, completedTask.id, nextWorkerName, completedTask.created_order);
2450
+ if (nextRow) {
2451
+ const nextTask = normalizeTodoRow(nextRow, 'active');
2452
+ const handoffText = handoffPrompt?.trim()
2453
+ || outputSummary?.trim()
2454
+ || `Continue from the completed work summary: ${completedTask.title}`;
2455
+ const existingPrompt = nextTask.task_prompt?.trim() || nextTask.details || nextTask.title;
2456
+ const mergedArtifactIds = Array.from(new Set([...(nextTask.artifact_ids || []), ...artifactIds]));
2457
+ const nextPrompt = [
2458
+ 'PREVIOUS WORKER HANDOFF',
2459
+ handoffText,
2460
+ '',
2461
+ 'ORIGINAL ASSIGNED TODO',
2462
+ existingPrompt,
2463
+ ].join('\n');
2464
+ db.prepare(`
2465
+ UPDATE todo_active
2466
+ SET task_prompt = ?,
2467
+ artifact_ids_json = ?,
2468
+ last_updated = ?,
2469
+ version = version + 1
2470
+ WHERE id = ?
2471
+ `).run(nextPrompt, JSON.stringify(mergedArtifactIds), now(), nextTask.id);
2472
+ handedOffTask = normalizeTodoRow(db.prepare('SELECT * FROM todo_active WHERE id = ?').get(nextTask.id), 'active');
2473
+ logTodoAuditTx(db, nextTask.id, 'active', 'edit', params.actor, {
2474
+ previousTaskId: completedTask.id,
2475
+ receivedHandoff: true,
2476
+ });
2477
+ }
2478
+ else {
2479
+ returnedToOrchestrator = true;
2480
+ logTodoAuditTx(db, taskId, 'completed', 'return_to_orchestrator', params.actor, {
2481
+ handoffPrompt,
2482
+ missingQueuedNextWorker: nextWorkerName,
2483
+ });
2484
+ }
2485
+ }
2486
+ else {
2487
+ returnedToOrchestrator = true;
2488
+ logTodoAuditTx(db, taskId, 'completed', 'return_to_orchestrator', params.actor, {
2489
+ handoffPrompt,
2490
+ });
2491
+ }
2492
+ }
2493
+ return {
2494
+ completedTask,
2495
+ insertedTask,
2496
+ handedOffTask,
2497
+ artifacts: artifactRows,
2498
+ returnedToOrchestrator,
2499
+ };
2500
+ });
2501
+ return tx();
2502
+ }
2503
+ function blockTodoTaskByWorker(taskId, params) {
2504
+ if (params.actor.actorType !== 'llm' && params.actor.actorType !== 'orchestrator') {
2505
+ throw new Error('Only worker LLMs or orchestrator can block workflow TODOs');
2506
+ }
2507
+ const blockerSummary = String(params.blockerSummary || '').trim();
2508
+ const checkedContext = String(params.checkedContext || '').trim();
2509
+ const userQuestion = String(params.userQuestion || '').trim();
2510
+ if (!blockerSummary)
2511
+ throw new Error('Blocked worker task requires blockerSummary');
2512
+ if (!checkedContext)
2513
+ throw new Error('Blocked worker task requires checkedContext');
2514
+ if (!userQuestion)
2515
+ throw new Error('Blocked worker task requires userQuestion');
2516
+ const db = getDb();
2517
+ const tx = db.transaction(() => {
2518
+ const row = db.prepare('SELECT * FROM todo_active WHERE id = ?').get(taskId);
2519
+ if (!row)
2520
+ throw new Error(`Task ${taskId} not found`);
2521
+ if (params.expectedVersion !== undefined && Number(row.version) !== Number(params.expectedVersion)) {
2522
+ throw new Error('Conflict: task version changed');
2523
+ }
2524
+ if (params.expectedLastUpdated !== undefined && String(row.last_updated) !== String(params.expectedLastUpdated)) {
2525
+ throw new Error('Conflict: task was updated by another actor');
2526
+ }
2527
+ const currentTask = normalizeTodoRow(row, 'active');
2528
+ const actorName = String(params.actor.actorId || '').trim();
2529
+ const actorBot = resolveAgentByName(actorName);
2530
+ const isAssignedWorker = Boolean(actorName &&
2531
+ currentTask.owner_name &&
2532
+ actorName.toLowerCase() === currentTask.owner_name.toLowerCase());
2533
+ const isOrchestratorActor = params.actor.actorType === 'orchestrator';
2534
+ if (!isAssignedWorker && !isOrchestratorActor) {
2535
+ throw new Error(`Actor "${actorName || 'unknown'}" is not the owner of task ${taskId}`);
2536
+ }
2537
+ const existingArtifactRows = loadArtifactRowsByIdsTx(db, currentTask.artifact_ids);
2538
+ const newArtifactRows = resolveArtifactRowsTx(db, params.artifactRefs || [], {
2539
+ projectId: currentTask.project_id,
2540
+ conversationId: currentTask.conversation_id,
2541
+ createdByBotId: actorBot?.id || currentTask.owner_bot_id || currentTask.created_by_bot_id,
2542
+ });
2543
+ const artifactRowsById = new Map();
2544
+ for (const item of [...existingArtifactRows, ...newArtifactRows])
2545
+ artifactRowsById.set(item.id, item);
2546
+ const artifactRows = Array.from(artifactRowsById.values()).sort((a, b) => a.id - b.id);
2547
+ const artifactIds = artifactRows.map((item) => item.id);
2548
+ db.prepare(`
2549
+ UPDATE todo_active
2550
+ SET blocker_summary = ?,
2551
+ blocker_checked = ?,
2552
+ blocker_question = ?,
2553
+ artifact_ids_json = ?,
2554
+ last_updated = ?,
2555
+ version = version + 1
2556
+ WHERE id = ?
2557
+ `).run(blockerSummary, checkedContext, userQuestion, JSON.stringify(artifactIds), now(), taskId);
2558
+ logTodoAuditTx(db, taskId, 'active', 'worker_blocked', params.actor, {
2559
+ blockerSummary,
2560
+ checkedContext,
2561
+ userQuestion,
2562
+ artifactIds,
2563
+ });
2564
+ logTodoAuditTx(db, taskId, 'active', 'return_to_orchestrator', params.actor, {
2565
+ blocked: true,
2566
+ blockerSummary,
2567
+ userQuestion,
2568
+ });
2569
+ const blockedTask = normalizeTodoRow(db.prepare('SELECT * FROM todo_active WHERE id = ?').get(taskId), 'active');
2570
+ const orchestratorBot = getOrchestratorBot();
2571
+ const ownerRole = String(actorBot?.role_label || actorBot?.role_class || currentTask.task_type || 'general').trim() || 'general';
2572
+ const effectivePolicy = getEffectiveOrchestrationPolicy(currentTask.project_id || undefined);
2573
+ const orchestratorPrompt = (0, orchestrator_blocked_prompt_1.buildBlockedWorkerOrchestratorPrompt)({
2574
+ orchestratorName: orchestratorBot?.name || 'Orchestrator',
2575
+ workerName: blockedTask.owner_name || actorName || 'Worker',
2576
+ workerRole: ownerRole,
2577
+ taskTitle: blockedTask.title,
2578
+ taskPrompt: blockedTask.task_prompt || blockedTask.details || '(not set)',
2579
+ projectId: blockedTask.project_id,
2580
+ projectName: blockedTask.project_id ? getProject(blockedTask.project_id)?.name || null : null,
2581
+ workspacePath: blockedTask.project_id ? getProject(blockedTask.project_id)?.folder || null : null,
2582
+ effectivePolicy,
2583
+ artifactRefs: artifactRows.map((item) => item.path_or_ref),
2584
+ blockerSummary,
2585
+ checkedContext,
2586
+ userQuestion,
2587
+ });
2588
+ return {
2589
+ blockedTask,
2590
+ artifacts: artifactRows,
2591
+ returnedToOrchestrator: true,
2592
+ orchestratorPrompt,
2593
+ };
2594
+ });
2595
+ return tx();
2596
+ }
1923
2597
  function deleteTodoTask(taskId, params) {
1924
2598
  if (params.actor.actorType !== 'user' && params.actor.actorType !== 'orchestrator')
1925
2599
  throw new Error('Only user or orchestrator can delete tasks');
@@ -2000,6 +2674,16 @@ function getLatestActiveTodoForConversation(conversationId) {
2000
2674
  return undefined;
2001
2675
  return normalizeTodoRow(row, 'active');
2002
2676
  }
2677
+ function getNextActiveTodoForConversation(conversationId) {
2678
+ const row = getDb().prepare('SELECT * FROM todo_active WHERE conversation_id = ? AND COALESCE(internal_only, 0) = 0 ORDER BY created_order ASC, id ASC LIMIT 1').get(conversationId);
2679
+ if (!row)
2680
+ return undefined;
2681
+ return normalizeTodoRow(row, 'active');
2682
+ }
2683
+ function listCompletedTodoTasksForConversation(conversationId) {
2684
+ const rows = getDb().prepare('SELECT * FROM todo_completed WHERE conversation_id = ? AND COALESCE(internal_only, 0) = 0 ORDER BY created_order ASC, id ASC').all(conversationId);
2685
+ return rows.map((row) => normalizeTodoRow(row, 'completed'));
2686
+ }
2003
2687
  exports.UNASSIGNED_PROJECT_NAME = 'unassigned';
2004
2688
  function createProject(p) {
2005
2689
  const db = getDb();
@@ -2097,7 +2781,7 @@ function getProjectOverview(projectId) {
2097
2781
  label: connection.label || null,
2098
2782
  status: connection.status,
2099
2783
  defaultModel: connection.default_model || null,
2100
- hasSecret: Boolean(connection.api_key_enc || connection.oauth_token),
2784
+ hasSecret: connection.access_mode === 'cli' || Boolean(connection.api_key_enc),
2101
2785
  }));
2102
2786
  const userPolicy = getUserOrchestrationPolicy() || null;
2103
2787
  const projectPolicyOverride = getProjectOrchestrationPolicyOverride(projectId) || null;