fuli-context 0.10.0 → 0.11.1

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 (212) hide show
  1. package/README.md +27 -30
  2. package/README.zh-CN.md +22 -26
  3. package/dist/web/assets/AboutPage-cW9hDghN.js +1 -0
  4. package/dist/web/assets/AgentConversations-CGznTWuV.js +2 -0
  5. package/dist/web/assets/AgentConversations-XqNEBY_2.css +1 -0
  6. package/dist/web/assets/AgentHand-DkyF4vgk.js +1 -0
  7. package/dist/web/assets/AgentName-C1mAVr-v.js +1 -0
  8. package/dist/web/assets/AgentProfilePage-pKUR0rfj.js +1 -0
  9. package/dist/web/assets/AgentsDirectoryPage-CUlbT2hD.js +1 -0
  10. package/dist/web/assets/AgentsDirectoryPage-yRWktiin.css +1 -0
  11. package/dist/web/assets/ConnectionsPage-CM9C-zEI.css +1 -0
  12. package/dist/web/assets/ConnectionsPage-CuDHIJXu.js +1 -0
  13. package/dist/web/assets/EmployeeRecruitDialog-CVbqCzHb.css +1 -0
  14. package/dist/web/assets/EmployeeRecruitDialog-D9QfGipd.js +1 -0
  15. package/dist/web/assets/EmployeeWorkbenchPage-D-c_Jm2z.css +1 -0
  16. package/dist/web/assets/EmployeeWorkbenchPage-DAtvGney.js +3 -0
  17. package/dist/web/assets/KnowledgeEditDialog-a-5rasG0.js +1 -0
  18. package/dist/web/assets/KnowledgeEditDialog-hT9znkoe.css +1 -0
  19. package/dist/web/assets/KnowledgeInspector-BGsqjNY2.js +1 -0
  20. package/dist/web/assets/KnowledgeOrganizerPage-BndgUsHS.js +1 -0
  21. package/dist/web/assets/KnowledgeOrganizerPage-DeL7Jp34.css +1 -0
  22. package/dist/web/assets/KnowledgePage-CQweO0YJ.js +1 -0
  23. package/dist/web/assets/KnowledgeWorkspace-BtUjKTLb.js +4 -0
  24. package/dist/web/assets/KnowledgeWorkspace-D4a7FcAb.css +1 -0
  25. package/dist/web/assets/NotFoundPage-BmQTTqQn.js +1 -0
  26. package/dist/web/assets/OverviewPage-By-fUzSG.css +1 -0
  27. package/dist/web/assets/OverviewPage-C73HyTsM.js +1 -0
  28. package/dist/web/assets/PersonalProfilePage-BMuOrppx.js +1 -0
  29. package/dist/web/assets/PersonalProfilePage-DrMDAAdy.css +1 -0
  30. package/dist/web/assets/PersonalProjectsPage-Cq9_kFvs.js +1 -0
  31. package/dist/web/assets/ProjectAgentFirstTask-Bi1ZsTC1.css +1 -0
  32. package/dist/web/assets/ProjectAgentFirstTask-CaHvOjkp.js +1 -0
  33. package/dist/web/assets/ProjectAgentsPage-DWGkgIsr.css +1 -0
  34. package/dist/web/assets/ProjectAgentsPage-WVcNdzVi.js +12 -0
  35. package/dist/web/assets/ProjectScopePicker-zAgiaZKL.js +1 -0
  36. package/dist/web/assets/PublicProjectsPage-DKamXDV9.css +1 -0
  37. package/dist/web/assets/PublicProjectsPage-DL093f65.js +1 -0
  38. package/dist/web/assets/ReviewPage-D_NIhPx8.js +1 -0
  39. package/dist/web/assets/RoundtablesPage-Cb0R4GaM.css +1 -0
  40. package/dist/web/assets/RoundtablesPage-CdVVyZE-.js +1 -0
  41. package/dist/web/assets/SettingsPage-BmYkLul4.css +1 -0
  42. package/dist/web/assets/SettingsPage-CbLaau8Q.js +1 -0
  43. package/dist/web/assets/SharedJefaReportPage-aygd6wvb.js +1 -0
  44. package/dist/web/assets/UiDialog-Q7fsJUoO.js +1 -0
  45. package/dist/web/assets/UiDisclosure-BS-muK5j.js +1 -0
  46. package/dist/web/assets/UiSegmented-CT28bxqw.js +1 -0
  47. package/dist/web/assets/UiSelect-Bv6R0yr1.css +1 -0
  48. package/dist/web/assets/UiSelect-DcGpPQQo.js +1 -0
  49. package/dist/web/assets/WritingTastePage-B_zYD1df.css +1 -0
  50. package/dist/web/assets/WritingTastePage-CYlwAFqV.js +1 -0
  51. package/dist/web/assets/agent-profile-BZj3ezWe.css +1 -0
  52. package/dist/web/assets/{attention-store-Dk-_P4Oi.js → attention-store-D-qIFRIb.js} +1 -1
  53. package/dist/web/assets/{catalog-DN72452f.js → catalog-UDv854JN.js} +1 -1
  54. package/dist/web/assets/client-DLS5FZBI.js +1 -0
  55. package/dist/web/assets/{console-BtPqo79j.js → console-CqzwSo7a.js} +1 -1
  56. package/dist/web/assets/{conversation-launcher-settings-jkHRmWai.js → conversation-launcher-settings-DRoIz42V.js} +1 -1
  57. package/dist/web/assets/{i18n-D2CJkEYK.js → i18n-b14l2pQA.js} +3 -3
  58. package/dist/web/assets/{identity-B3zEw-1u.js → identity-BWbPmZZf.js} +1 -1
  59. package/dist/web/assets/index-D1OHIz4c.js +5 -0
  60. package/dist/web/assets/index-kPB0Cf8Q.css +1 -0
  61. package/dist/web/assets/{model-S7ehAjky.js → model-DupZy8Qb.js} +1 -1
  62. package/dist/web/assets/{personal-profile-graph--GG3Mizo.js → personal-profile-graph-BPTtMJTZ.js} +1 -1
  63. package/dist/web/assets/shared-report-BYnLhmCZ.js +1 -0
  64. package/dist/web/assets/{useAgentRoster-BAPKWZ9y.js → useAgentRoster-ep7SpKv1.js} +1 -1
  65. package/dist/web/assets/{useApi-CROJJdhE-BtFMXFjw.js → useApi-CROJJdhE-CEQAEuug.js} +1 -1
  66. package/dist/web/assets/{vue-router-DkaTtPn0.js → vue-router-B6ZjK-Br.js} +1 -1
  67. package/dist/web/index.html +14 -16
  68. package/docs/agent-conversations-and-collaboration.md +9 -9
  69. package/docs/agent-roundtable.md +77 -0
  70. package/docs/employee-agents.md +2 -2
  71. package/docs/project-agent-memory.md +1 -1
  72. package/graph-provider/fuli_graph/agent_conversation_models.py +11 -2
  73. package/graph-provider/fuli_graph/conversation_context.py +40 -9
  74. package/graph-provider/fuli_graph/default_project_lead.py +88 -0
  75. package/graph-provider/fuli_graph/project_agent_context_models.py +5 -1
  76. package/graph-provider/fuli_graph/project_agent_delegations.py +118 -0
  77. package/graph-provider/fuli_graph/project_agent_employee_numbers.py +41 -0
  78. package/graph-provider/fuli_graph/project_agent_memory_routes.py +20 -0
  79. package/graph-provider/fuli_graph/project_agent_models.py +1 -0
  80. package/graph-provider/fuli_graph/project_agent_task_models.py +3 -0
  81. package/graph-provider/fuli_graph/project_agent_team_growth.py +2 -2
  82. package/graph-provider/fuli_graph/runtime.py +10 -76
  83. package/graph-provider/fuli_graph/store.py +2 -0
  84. package/graph-provider/fuli_graph/store_agent_conversations.py +60 -8
  85. package/graph-provider/fuli_graph/store_project_agent_context.py +52 -4
  86. package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +16 -1
  87. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +3 -1
  88. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +3 -2
  89. package/graph-provider/fuli_graph/store_project_agent_tasks.py +12 -8
  90. package/graph-provider/fuli_graph/store_project_agents.py +56 -129
  91. package/graph-provider/fuli_graph/store_task_contexts.py +26 -0
  92. package/graph-provider/fuli_graph/system_agents.py +130 -0
  93. package/graph-provider/fuli_graph/system_hr_identity.py +1 -1
  94. package/graph-provider/fuli_graph/task_context_routes.py +10 -0
  95. package/graph-provider/pyproject.toml +1 -1
  96. package/npm-shrinkwrap.json +2 -2
  97. package/package.json +2 -2
  98. package/src/agent-roundtable/http.js +14 -0
  99. package/src/agent-roundtable/service.js +247 -0
  100. package/src/agent-roundtable/store.js +86 -0
  101. package/src/agent-roundtable/tools.js +61 -0
  102. package/src/agent-roundtable/wake.js +100 -0
  103. package/src/agent-tools/agent-conversation-definitions.js +3 -3
  104. package/src/agent-tools/graph-definitions.js +3 -2
  105. package/src/agent-tools/interface-catalog.js +0 -5
  106. package/src/agent-tools.js +2 -2
  107. package/src/agents/claude-code/wake.js +37 -0
  108. package/src/agents/codex/wake.js +47 -0
  109. package/src/agents/identity-receipt.js +35 -7
  110. package/src/agents/session-log.js +34 -0
  111. package/src/agents/wake-registry.js +14 -0
  112. package/src/cli/command-registry.js +0 -2
  113. package/src/cli/setup-command.js +2 -5
  114. package/src/cli/update-command.js +13 -2
  115. package/src/cli.js +0 -5
  116. package/src/employees/management-policy.js +4 -2
  117. package/src/employees/manifest.js +3 -0
  118. package/src/employees/recruitment.js +3 -2
  119. package/src/graphiti/agent-conversations.js +11 -9
  120. package/src/graphiti/agent-knowledge-workflows.js +34 -7
  121. package/src/graphiti/federated-application.js +17 -5
  122. package/src/graphiti/project-agent-mapping.js +2 -0
  123. package/src/graphiti/project-agent-task-entry.js +38 -8
  124. package/src/graphiti/project-agent-team-context.js +4 -1
  125. package/src/graphiti/project-path-context.js +7 -2
  126. package/src/graphiti/provider-client.js +17 -0
  127. package/src/graphiti/repository-project-registration.js +17 -0
  128. package/src/graphiti/task-entry-preferences.js +17 -4
  129. package/src/graphiti/task-entry-project-scope.js +67 -0
  130. package/src/http/api-router.js +5 -3
  131. package/src/http/error-mapping.js +0 -2
  132. package/src/mcp/instructions.js +5 -5
  133. package/src/mcp/session-id.js +1 -1
  134. package/src/mcp/tool-annotations.js +3 -4
  135. package/src/setup/agents.js +1 -41
  136. package/src/setup/graph-runtime.js +15 -2
  137. package/src/setup/session-skill.js +22 -2
  138. package/dist/web/assets/AboutPage-D-GyJNv1.js +0 -1
  139. package/dist/web/assets/AgentConversations-DOuZgUeA.js +0 -2
  140. package/dist/web/assets/AgentConversations-DS8h3yKn.css +0 -1
  141. package/dist/web/assets/AgentHand-eGsXoGSq.js +0 -1
  142. package/dist/web/assets/AgentName-Dtmu9Ff5.js +0 -1
  143. package/dist/web/assets/AgentProfilePage-D8Tvayn7.js +0 -1
  144. package/dist/web/assets/AgentsDirectoryPage-BTEfKZjF.js +0 -1
  145. package/dist/web/assets/ConnectionsPage-C1w99yN3.css +0 -1
  146. package/dist/web/assets/ConnectionsPage-DwOIcvl9.js +0 -1
  147. package/dist/web/assets/EmployeeRecruitDialog-BrIMRblq.css +0 -1
  148. package/dist/web/assets/EmployeeRecruitDialog-D11blfwU.js +0 -1
  149. package/dist/web/assets/EmployeeWorkbenchPage-BcEzYt65.css +0 -1
  150. package/dist/web/assets/EmployeeWorkbenchPage-CK2BAoGt.js +0 -3
  151. package/dist/web/assets/GrowthLoading-KTjE2e50.js +0 -1
  152. package/dist/web/assets/KnowledgeEditDialog-B3x8vs3c.css +0 -1
  153. package/dist/web/assets/KnowledgeEditDialog-n9trHQ5d.js +0 -1
  154. package/dist/web/assets/KnowledgeInspector-BcNjgw9H.js +0 -1
  155. package/dist/web/assets/KnowledgeOrganizerPage-BzgxP-nG.css +0 -1
  156. package/dist/web/assets/KnowledgeOrganizerPage-DMBpGxLr.js +0 -1
  157. package/dist/web/assets/KnowledgePage-POkCMW8f.js +0 -1
  158. package/dist/web/assets/KnowledgeWorkspace-2FvQadhB.js +0 -4
  159. package/dist/web/assets/KnowledgeWorkspace-sb8OJ97I.css +0 -1
  160. package/dist/web/assets/NotFoundPage-BCMZbeB8.js +0 -1
  161. package/dist/web/assets/OverviewPage-0CFWCG9-.css +0 -1
  162. package/dist/web/assets/OverviewPage-0mYifpjp.js +0 -1
  163. package/dist/web/assets/PersonalProfilePage-Chv2a2pi.css +0 -1
  164. package/dist/web/assets/PersonalProfilePage-JW6Muix6.js +0 -1
  165. package/dist/web/assets/PersonalProjectsPage-CRbi64JP.js +0 -1
  166. package/dist/web/assets/ProjectAgentFirstTask-6afJ8HUe.css +0 -1
  167. package/dist/web/assets/ProjectAgentFirstTask-DJ0wTbh8.js +0 -1
  168. package/dist/web/assets/ProjectAgentsPage-CPgKVLgg.js +0 -12
  169. package/dist/web/assets/ProjectAgentsPage-DeuRkMp1.css +0 -1
  170. package/dist/web/assets/ProjectScopePicker-BwB7gK-7.js +0 -1
  171. package/dist/web/assets/PublicProjectsPage-CB272Sq-.js +0 -1
  172. package/dist/web/assets/ReviewPage-CelTZU4C.js +0 -1
  173. package/dist/web/assets/RoundtablesPage-C9dXfWCl.js +0 -2
  174. package/dist/web/assets/RoundtablesPage-DCbIBnBq.css +0 -1
  175. package/dist/web/assets/SearchableSelect-f7EHbyDL.js +0 -1
  176. package/dist/web/assets/SettingsPage-5g7mRN2F.css +0 -1
  177. package/dist/web/assets/SettingsPage-nAijmm5f.js +0 -1
  178. package/dist/web/assets/SharedJefaReportPage-CjpJ9bkk.js +0 -1
  179. package/dist/web/assets/UiDisclosure-CWlC91Zt.js +0 -1
  180. package/dist/web/assets/WritingTastePage-DaHWY61P.js +0 -1
  181. package/dist/web/assets/WritingTastePage-SMGF39mf.css +0 -1
  182. package/dist/web/assets/agent-profile-CBvJHGjt.css +0 -1
  183. package/dist/web/assets/client-B2cRl-7c.js +0 -1
  184. package/dist/web/assets/copy-D2LqwtPZ.js +0 -1
  185. package/dist/web/assets/index-CNp0tRNp.css +0 -1
  186. package/dist/web/assets/index-ulISYH02.js +0 -5
  187. package/dist/web/assets/shared-report-DEK9wvhl.js +0 -1
  188. package/dist/web/assets/useModalDialog-D8tiJfbc.js +0 -1
  189. package/docs/roundtable.md +0 -46
  190. package/src/agents/a2a/roundtable-participant.js +0 -374
  191. package/src/agents/claude-code/roundtable-participant.js +0 -24
  192. package/src/agents/codex/roundtable-config.js +0 -32
  193. package/src/agents/codex/roundtable-participant.js +0 -34
  194. package/src/agents/grok/roundtable-participant.js +0 -185
  195. package/src/agents/pi/roundtable-participant.js +0 -193
  196. package/src/agents/pi/roundtable-result-format.js +0 -183
  197. package/src/cli/roundtable-command.js +0 -79
  198. package/src/roundtables/application.js +0 -69
  199. package/src/roundtables/domain.js +0 -181
  200. package/src/roundtables/http-router.js +0 -58
  201. package/src/roundtables/mcp-server.js +0 -55
  202. package/src/roundtables/participant-registry.js +0 -12
  203. package/src/roundtables/process.js +0 -70
  204. package/src/roundtables/profiles.js +0 -84
  205. package/src/roundtables/result-contract.js +0 -47
  206. package/src/roundtables/server.js +0 -52
  207. package/src/roundtables/service.js +0 -480
  208. package/src/roundtables/store.js +0 -63
  209. package/src/roundtables/tool-contract.js +0 -58
  210. package/src/roundtables/turn-prompt.js +0 -24
  211. package/src/roundtables/worker.js +0 -85
  212. /package/dist/web/assets/{GrowthLoading-CIuQ1icy.css → client-CIuQ1icy.css} +0 -0
@@ -12,20 +12,48 @@ export function agentContinuation(projectId, agentId, conversationId = null) {
12
12
  guidance: 'Use a connected client and the same authorized project/Agent. Obtain a fresh task context; never reuse the previous client’s token. This is a FULI prompt, not a native client @ picker registration.' };
13
13
  }
14
14
 
15
- export function taskAgentReceipt(application, { projectId, agentId, name = null,
16
- sourceApplication = null, workStatus = null, conversationId = null, persistence = null }) {
17
- const owner = agentId ? { agent_id: agentId, name,
18
- profile_url: agentProfileUrl(application, agentId), source_application: sourceApplication } : null;
15
+ function agentLink(application, agentId, name) {
19
16
  const label = String(name?.trim() || agentId).replace(/[\r\n\t]+/g, ' ')
20
17
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
21
18
  .replace(/[\\`*_{}\[\]()!|]/g, '\\$&');
19
+ return `[${label}](<${agentProfileUrl(application, agentId).replace(/[<>\s]/g, encodeURIComponent)}>)`;
20
+ }
21
+
22
+ // The reply's attribution line: who led the task and who actually helped.
23
+ export function taskAgentReceipt(application, { projectId, agentId, name = null,
24
+ sourceApplication = null, workStatus = null, conversationId = null, persistence = null,
25
+ collaborators = [], assignmentStatus = null, assignmentReason = null }) {
26
+ const owner = agentId ? { agent_id: agentId, name,
27
+ profile_url: agentProfileUrl(application, agentId), source_application: sourceApplication } : null;
28
+ const helpers = owner ? collaborators.filter(item => item.agentId !== agentId) : [];
22
29
  const markdown = owner
23
- ? `FULI Agent:[${label}](<${owner.profile_url.replace(/[<>\s]/g, encodeURIComponent)}>)`
24
- : 'FULI Agent:未选定';
30
+ ? `FULI Agent:负责人 ${agentLink(application, agentId, name)}`
31
+ + (helpers.length ? ` · 协作 ${helpers.map(item => agentLink(application, item.agentId, item.name)).join('、')}` : '')
32
+ : unassignedReceipt(assignmentStatus, assignmentReason);
25
33
  return { required: true, markdown, owner,
34
+ collaborators: helpers.map(item => ({ agent_id: item.agentId, name: item.name,
35
+ profile_url: agentProfileUrl(application, item.agentId) })),
26
36
  personal_project_id: projectId, work_status: workStatus, persistence,
27
37
  continuation: owner ? agentContinuation(projectId, agentId, conversationId) : null,
28
- guidance: 'MUST include agent_receipt.markdown unchanged exactly once in every final user-visible reply, including greetings, status and errors, even when capture is disabled or nothing is retained. Use this task’s receipt; never invent an owner. Report only truthful work/memory status and actually used collaboration_receipt participants; tools and configured peers are not independent model workers.' };
38
+ guidance: 'MUST end every final user-visible reply with agent_receipt.markdown unchanged, exactly once, including greetings, status and errors, even when capture is disabled or nothing is retained. The receipt returned by checkpoint_task_knowledge supersedes the entry receipt because it names actual collaborators. Never invent an owner or a collaborator.' };
39
+ }
40
+
41
+ function unassignedReceipt(status, reason) {
42
+ if (!status) return 'FULI Agent:分配未完成(任务没有可用负责人)';
43
+ if (status === 'project_unresolved') {
44
+ return reason === 'ambiguous' ? 'FULI Agent:分配待确认(项目归属待确认)'
45
+ : reason === 'session_identity_required' ? 'FULI Agent:分配失败(缺少会话标识)'
46
+ : 'FULI Agent:分配失败(项目归属未能恢复)';
47
+ }
48
+ const labels = {
49
+ unavailable: '分配失败(服务或身份恢复不可用)',
50
+ agent_unavailable: '分配失败(指定 FLA 不可用)',
51
+ agent_not_found: '分配失败(指定 FLA 不存在)',
52
+ ambiguous_agent: '分配待确认(存在同名 FLA)',
53
+ agent_selection_conflict: '分配待确认(FLA 选择冲突)',
54
+ manual_selection: '分配待确认(需要选择负责人)'
55
+ };
56
+ return `FULI Agent:${labels[status] ?? '分配未完成(HR 尚未返回负责人)'}`;
29
57
  }
30
58
 
31
59
  export function employeeCollaborationReceipt(application, context, { tool, permission, sourceApplication }) {
@@ -0,0 +1,34 @@
1
+ import { closeSync, openSync, readSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ // Read-only helpers for locating a client's JSONL session log on disk.
5
+
6
+ export function listDirectory(directory) {
7
+ try { return readdirSync(directory); } catch { return []; }
8
+ }
9
+
10
+ // Newest-first search for a file ending in `suffix`, at most `depth` levels down.
11
+ export function findFile(directory, suffix, depth) {
12
+ for (const entry of listDirectory(directory).sort().reverse()) {
13
+ const path = join(directory, entry);
14
+ if (entry.endsWith(suffix)) return path;
15
+ if (depth > 0 && !entry.includes('.')) {
16
+ const found = findFile(path, suffix, depth - 1);
17
+ if (found) return found;
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+
23
+ // The first non-empty string `pick` returns for a JSON line near the start of the log.
24
+ export function firstField(file, pick) {
25
+ const handle = openSync(file, 'r');
26
+ try {
27
+ const buffer = Buffer.alloc(256 * 1024);
28
+ const length = readSync(handle, buffer, 0, buffer.length, 0);
29
+ for (const line of buffer.subarray(0, length).toString('utf8').split('\n')) {
30
+ try { const value = pick(JSON.parse(line)); if (typeof value === 'string' && value) return value; } catch { /* partial line */ }
31
+ }
32
+ return null;
33
+ } finally { closeSync(handle); }
34
+ }
@@ -0,0 +1,14 @@
1
+ import { claudeCodeWake } from './claude-code/wake.js';
2
+ import { codexWake } from './codex/wake.js';
3
+
4
+ // Clients that can be woken headlessly to answer another Agent, in preference order.
5
+ export const WAKE_ADAPTERS = Object.freeze({
6
+ [claudeCodeWake.id]: claudeCodeWake,
7
+ [codexWake.id]: codexWake,
8
+ });
9
+
10
+ export const WAKE_CLIENTS = Object.freeze(Object.keys(WAKE_ADAPTERS));
11
+
12
+ export function wakeAdapter(client) {
13
+ return Object.hasOwn(WAKE_ADAPTERS, client) ? WAKE_ADAPTERS[client] : null;
14
+ }
@@ -12,8 +12,6 @@ General:
12
12
  --version, -v
13
13
 
14
14
  Local service:
15
- roundtable serve [--port PORT] [--data-dir DIR] [--public-url HTTPS_URL]
16
- roundtable worker --url URL --room ID --runtime codex|claude-code|pi|grok|a2a [--workspace DIR] [--allow-write] [--model MODEL]
17
15
  start [--port PORT] [--open] [--lan|--no-lan] [--rebuild] [--data-dir DIR] [--personal-space NAME] (checks Agent setup)
18
16
  stop [--data-dir DIR]
19
17
  restart [--port PORT] [--open] [--lan|--no-lan] [--rebuild] [--data-dir DIR] [--personal-space NAME]
@@ -96,12 +96,9 @@ export function formatSetupResult(result) {
96
96
  }
97
97
  lines.push('Knowledge storage: Graphiti / Neo4j');
98
98
  for (const agent of result.agents) {
99
- if (agent.status === 'partial') {
100
- lines.push(`${agent.label}: partially configured; retry setup to finish installation`);
101
- continue;
102
- }
103
99
  if (agent.status !== 'connected') {
104
- lines.push(`${agent.label}: connection failed; retry setup later`);
100
+ const state = agent.status === 'partial' ? 'partially configured' : 'connection failed';
101
+ lines.push(`${agent.label}: ${state}${agent.message ? ` (${agent.message})` : ''}; retry fuli setup`);
105
102
  continue;
106
103
  }
107
104
  lines.push(agent.newTaskRequired
@@ -234,11 +234,21 @@ function describeProcessFailure(result) {
234
234
  return 'unknown process error';
235
235
  }
236
236
 
237
+ // Node refuses to spawn Windows batch files such as npm.cmd without a shell
238
+ // (CVE-2024-27980). Only fixed, shell-inert arguments may take that path.
239
+ export function batchFileSpawnOptions(command, args) {
240
+ if (!/\.(?:cmd|bat)$/i.test(command)) return {};
241
+ const unsafe = args.find((arg) => !/^[\w@./:=-]+$/.test(arg));
242
+ if (unsafe !== undefined) throw new TypeError(`Refusing to pass a shell-sensitive argument to ${command}`);
243
+ return { shell: true };
244
+ }
245
+
237
246
  function spawnInherited(command, args, { env }) {
238
247
  return spawnSync(command, args, {
239
248
  env,
240
249
  stdio: 'inherit',
241
- windowsHide: true
250
+ windowsHide: true,
251
+ ...batchFileSpawnOptions(command, args)
242
252
  });
243
253
  }
244
254
 
@@ -247,7 +257,8 @@ function spawnCaptured(command, args, { env }) {
247
257
  env,
248
258
  encoding: 'utf8',
249
259
  stdio: ['ignore', 'pipe', 'pipe'],
250
- windowsHide: true
260
+ windowsHide: true,
261
+ ...batchFileSpawnOptions(command, args)
251
262
  });
252
263
  }
253
264
 
package/src/cli.js CHANGED
@@ -10,7 +10,6 @@ import { runLocalRuntimeCommand } from './cli/local-runtime-command.js';
10
10
  import { runRemoteMcpCommand } from './cli/remote-mcp-command.js';
11
11
  import { runGraphDataCommand } from './cli/graph-data-command.js';
12
12
  import { runEmployeeCommand } from './cli/employee-command.js';
13
- import { runRoundtableCommand } from './cli/roundtable-command.js';
14
13
  import { runSetupCommand } from './cli/setup-command.js';
15
14
  import { runUninstallCommand } from './cli/uninstall-command.js';
16
15
  import { runUpdateCommand } from './cli/update-command.js';
@@ -42,10 +41,6 @@ export async function main(argv = process.argv.slice(2), env = process.env) {
42
41
  assertSupportedNodeVersion();
43
42
  return runEmployeeCommand(commandArgs, { env });
44
43
  }
45
- if (command === 'roundtable') {
46
- assertSupportedNodeVersion();
47
- return runRoundtableCommand(commandArgs, { env });
48
- }
49
44
  if (command === 'connect-workspace') {
50
45
  assertSupportedNodeVersion();
51
46
  return runWorkspaceConnectionCommand(commandArgs, { env });
@@ -2,7 +2,7 @@ import { mkdirSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
3
  import { DatabaseSync } from 'node:sqlite';
4
4
  import { z } from 'zod';
5
- import { EmployeeError } from './manifest.js';
5
+ import { EmployeeError, FIXED_EMPLOYEE_AGENT_TYPES } from './manifest.js';
6
6
 
7
7
  const projectId = z.string().min(1).max(256);
8
8
  export const employeeManagementSchema = z.object({
@@ -73,7 +73,9 @@ export function managementFor(manifest, agent, stored) {
73
73
  }
74
74
  // Upgrades preserve existing authorization. Only a new recruitment uses the
75
75
  // template default; an existing employee opts in by saving the new all rule.
76
- return { revision: 0, mode: agent ? 'selected' : manifest.defaultProjectScope ?? 'selected',
76
+ // A fixed role nobody has assigned yet is still in its template default.
77
+ const configured = agent && !(Object.hasOwn(FIXED_EMPLOYEE_AGENT_TYPES, manifest.id) && !agent.assignments?.length);
78
+ return { revision: 0, mode: configured ? 'selected' : manifest.defaultProjectScope ?? 'selected',
77
79
  projectIds: [...new Set((agent?.assignments ?? []).filter(a => a.status === 'active').map(a => a.personalProjectId))],
78
80
  excludedProjectIds: [], titleMode: manifest.taskEntry ? 'auto' : 'suggest', titleStyle: 'emoji' };
79
81
  }
@@ -44,6 +44,9 @@ export function parseEmployeeManifest(value) {
44
44
  return parsed.data;
45
45
  }
46
46
 
47
+ // Built-in roles every personal space has; the Provider provisions their identities.
48
+ export const FIXED_EMPLOYEE_AGENT_TYPES = Object.freeze({ jefa: 'coordinator', bole: 'hr' });
49
+
47
50
  export const employeeAgentId = (id) => `employee.${identifier.parse(id)}`;
48
51
  export const employeeCapability = (id) => `fuli.employee:${identifier.parse(id)}`;
49
52
 
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { EmployeeError, employeeAgentId, employeeCapability } from './manifest.js';
2
+ import { EmployeeError, FIXED_EMPLOYEE_AGENT_TYPES, employeeAgentId, employeeCapability } from './manifest.js';
3
3
  import {
4
4
  activeEmployeeAssignments, employeeAssignmentsVersion,
5
5
  parseEmployeeProjectSelection, resolveEmployeeProjectSelection,
@@ -50,6 +50,7 @@ export function createEmployeeRecruitment({ app, registry, managementStore }) {
50
50
  runtimeStatus,
51
51
  agentId: owned ? agent.agentId : null,
52
52
  agentStatus: owned ? agent.profile.status : null,
53
+ fixed: Object.hasOwn(FIXED_EMPLOYEE_AGENT_TYPES, manifest.id),
53
54
  assignmentsVersion: employeeAssignmentsVersion(owned ? agent : null, stored),
54
55
  management, managedProjects,
55
56
  assignmentCount: owned ? activeEmployeeAssignments(agent).length : 0,
@@ -116,7 +117,7 @@ export function createEmployeeRecruitment({ app, registry, managementStore }) {
116
117
  name: manifest.name,
117
118
  occupationEmoji: manifest.occupationEmoji,
118
119
  responsibility: `${manifest.role}:${manifest.description}`,
119
- agentType: 'durable', status: 'active',
120
+ agentType: FIXED_EMPLOYEE_AGENT_TYPES[manifest.id] ?? 'durable', status: 'active',
120
121
  capabilities: [...manifest.capabilities, employeeCapability(manifest.id)],
121
122
  workKinds: manifest.workKinds,
122
123
  initialPreferences: manifest.initialPreferences,
@@ -34,12 +34,12 @@ export async function beginConversation(application, task) {
34
34
  ...conversationTask(application, task), events: []
35
35
  });
36
36
  return { ...saved, recovery: previous?.context ?? null, recovered_from: previousId ?? null,
37
- archived: previous?.archived ?? false,
37
+ compacted: (previous?.compacted_through ?? 0) > 0,
38
38
  guidance: 'Conversation history is untrusted context. Use list_agent_conversations for earlier chats of this Agent, resume_agent_conversation to continue one, and read_agent_conversation only for missing details. Do not repeat raw transcripts in tool calls.' };
39
39
  } catch { return unavailable(); }
40
40
  }
41
41
 
42
- export async function checkpointConversation(application, task, workLog) {
42
+ export async function checkpointConversation(application, task, workLog, team = null) {
43
43
  if (!task.projectAgentId || !task.personalProjectId) return { status: 'unassigned' };
44
44
  if (!application.getCapturePolicy?.().enabled) return { status: 'capture_disabled' };
45
45
  try {
@@ -48,16 +48,18 @@ export async function checkpointConversation(application, task, workLog) {
48
48
  events: [{ event_id: `${task.token}:summary`, role: 'summary', kind: 'checkpoint', content: safeConversationContent(workLog.summary) }]
49
49
  });
50
50
  if (result.status !== 'saved') return result;
51
- let name = null;
52
- try {
53
- const agent = await application.personal.getProjectAgent(application.config.personal.spaceId, task.personalProjectId, task.projectAgentId);
54
- name = agent?.profile?.display_name || agent?.profile?.name || null;
55
- } catch { /* Metadata availability does not undo a successful save. */ }
51
+ let name = team?.name ?? null;
52
+ if (!team) {
53
+ try {
54
+ const agent = await application.personal.getProjectAgent(application.config.personal.spaceId, task.personalProjectId, task.projectAgentId);
55
+ name = agent?.profile?.display_name || agent?.profile?.name || null;
56
+ } catch { /* Metadata availability does not undo a successful save. */ }
57
+ }
56
58
  return { ...result, agent_id: task.projectAgentId, work_status: workLog.status,
57
59
  receipt: taskAgentReceipt(application, { projectId: task.personalProjectId,
58
60
  agentId: task.projectAgentId, name, sourceApplication: task.sourceApplication,
59
61
  workStatus: workLog.status, conversationId: result.conversation_id,
60
- persistence: result.status }),
62
+ persistence: result.status, collaborators: team?.collaborators ?? [] }),
61
63
  coverage: 'task_summary; visible transcript capture is confirmed separately by the host hook',
62
64
  guidance: 'Include the Agent name, truthful work status and conversation ID in the final receipt. In another client select this Agent, then call resume_agent_conversation with its current taskContextToken and this conversation ID.' };
63
65
  } catch { return unavailable(); }
@@ -84,6 +86,6 @@ export async function resumeConversation(application, input) {
84
86
  }
85
87
  export async function updateConversationPolicy(application, input) {
86
88
  return application.personal.conversation('policy', { ...conversationScope(application, input), policy: {
87
- idle_days: input.idleDays, context_budget: input.contextBudget, enabled: input.enabled
89
+ compact_after_kb: input.compactAfterKb, context_budget: input.contextBudget, enabled: input.enabled
88
90
  } });
89
91
  }
@@ -1,4 +1,5 @@
1
1
  import { beginConversation, checkpointConversation } from './agent-conversations.js';
2
+ import { taskAgentReceipt } from '../agents/identity-receipt.js';
2
3
  import { createHash } from 'node:crypto';
3
4
 
4
5
  import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
@@ -92,7 +93,8 @@ export async function checkpointTaskKnowledge(application, {
92
93
  workLog = null,
93
94
  sourceApplication = 'other',
94
95
  personalProjectId = null,
95
- remoteSessionId = null
96
+ remoteSessionId = null,
97
+ collaborators = []
96
98
  }) {
97
99
  if (!['capture_candidates', 'retain_nothing'].includes(disposition)) {
98
100
  throw validationError('Unknown task knowledge checkpoint disposition');
@@ -122,11 +124,21 @@ export async function checkpointTaskKnowledge(application, {
122
124
  'Task checkpoint already has different input; resume the original review or begin a new task context'
123
125
  );
124
126
  }
127
+ const team = await taskTeam(application, task, collaborators);
128
+ const finish = async (result) => {
129
+ const effectiveLog = workLog ?? { status: 'reported', summary: reason };
130
+ const conversation = await checkpointConversation(application, task, effectiveLog, team);
131
+ return { ...result, conversation,
132
+ agent_receipt: taskAgentReceipt(application, { projectId: task.personalProjectId,
133
+ agentId: task.projectAgentId ?? null, name: team.name, sourceApplication: task.sourceApplication,
134
+ workStatus: effectiveLog.status, conversationId: conversation?.conversation_id ?? null,
135
+ persistence: conversation?.status ?? null, collaborators: team.collaborators }),
136
+ ...(team.ignored.length ? { ignored_collaborators: team.ignored } : {}) };
137
+ };
125
138
  if (task.checkpoint?.phase === 'complete') {
126
- return { status: 'checkpointed', disposition, reason,
139
+ return finish({ status: 'checkpointed', disposition, reason,
127
140
  personal_project_id: task.personalProjectId, project_agent_id: task.projectAgentId,
128
- replayed: true, capture_status: task.checkpoint.captureStatus,
129
- conversation: await checkpointConversation(application, task, workLog ?? { status: 'reported', summary: reason }) };
141
+ replayed: true, capture_status: task.checkpoint.captureStatus });
130
142
  }
131
143
  const captureInput = capture ? {
132
144
  ...capture,
@@ -181,21 +193,36 @@ export async function checkpointTaskKnowledge(application, {
181
193
  await application.taskContextRegistry.checkpoint(taskContextToken, {
182
194
  ...checkpoint, captureStatus: captureResult?.status ?? null
183
195
  }, sourceApplication);
184
- return {
196
+ return finish({
185
197
  status: 'checkpointed',
186
198
  disposition,
187
199
  reason,
188
200
  personal_project_id: task.personalProjectId,
189
201
  project_agent_id: task.projectAgentId ?? null,
190
202
  capture: captureResult,
191
- conversation: await checkpointConversation(application, task, workLog ?? { status: 'reported', summary: reason }),
192
203
  agent_memory: memoryResult ? {
193
204
  status: memoryResult.status, agentId: memoryResult.agentId,
194
205
  checkpointId: memoryResult.checkpointId, revision: memoryResult.revision,
195
206
  sourceApplication: memoryResult.sourceApplication,
196
207
  sourceSessionId: memoryResult.sourceSessionId, createdAt: memoryResult.createdAt
197
208
  } : null
198
- };
209
+ });
210
+ }
211
+
212
+ // Names the lead and only collaborators that are real, active Agents of this space.
213
+ async function taskTeam(application, task, collaboratorIds) {
214
+ const requested = [...new Set(collaboratorIds)].filter(id => id !== task.projectAgentId);
215
+ if (!task.projectAgentId) return { name: null, collaborators: [], ignored: requested };
216
+ let agents = [];
217
+ try {
218
+ agents = await application.listProjectAgents({ personalSpaceId: application.config.personal.spaceId });
219
+ } catch { /* Attribution metadata never blocks a checkpoint. */ }
220
+ const byId = new Map(agents.map(agent => [agent.agentId, agent]));
221
+ const label = agent => agent?.profile.displayName || agent?.profile.name || null;
222
+ const active = requested.filter(id => byId.get(id)?.profile.status === 'active');
223
+ return { name: label(byId.get(task.projectAgentId)),
224
+ collaborators: active.map(id => ({ agentId: id, name: label(byId.get(id)) })),
225
+ ignored: requested.filter(id => !active.includes(id)) };
199
226
  }
200
227
 
201
228
  function canonicalCheckpoint(value) {
@@ -1,6 +1,7 @@
1
- import { dirname } from 'node:path';
1
+ import { dirname, join } from 'node:path';
2
2
  import { createEmployeeService } from '../employees/service.js';
3
- import { createFuliRoundtableService } from '../roundtables/application.js';
3
+ import { createAgentRoundtable } from '../agent-roundtable/service.js';
4
+ import { createRoundtableStore } from '../agent-roundtable/store.js';
4
5
 
5
6
  import { GraphitiProviderClient } from './provider-client.js';
6
7
  import {
@@ -79,6 +80,8 @@ import {
79
80
  } from './related-project-suggestions.js';
80
81
  import { buildUserTasteSkill } from './user-taste-skill.js';
81
82
  import { taskEntryPreferences } from './task-entry-preferences.js';
83
+ import { TASK_ENTRY_TOOLS, registerRepositoryProject } from './repository-project-registration.js';
84
+ import { resolveTaskEntryProject } from './task-entry-project-scope.js';
82
85
  import { getWritingTasteProfile as getWritingTasteProfileWorkflow } from './writing-taste-profile-workflow.js';
83
86
  import {
84
87
  groupSubscriptions,
@@ -117,7 +120,8 @@ export function openFederatedGraphApplication({
117
120
  fetchImpl
118
121
  });
119
122
  app.employees = createEmployeeService({ app, runtimeConfigPath });
120
- app.roundtables = createFuliRoundtableService({ app, dataDir: dirname(runtimeConfigPath) });
123
+ app.roundtable = createAgentRoundtable({ app, env,
124
+ openStore: () => createRoundtableStore(join(dirname(runtimeConfigPath), 'agent-roundtable.sqlite')) });
121
125
  }
122
126
  return app;
123
127
  }
@@ -258,7 +262,15 @@ export class FederatedGraphApplication extends ProjectAgentControlPlaneApplicati
258
262
  agentInvocation = false,
259
263
  agentToolName = 'get_collaboration_preferences'
260
264
  } = {}) {
261
- const projectResolution = await this.#resolvePreferenceProject({ personalProjectId, projectPath });
265
+ let projectResolution = await this.#resolvePreferenceProject({ personalProjectId, projectPath });
266
+ if (projectResolution.repositoryProjectId && agentInvocation && TASK_ENTRY_TOOLS.has(agentToolName)
267
+ && this.getCapturePolicy().enabled) {
268
+ // Every task gets a FULI Agent, so a task in an unregistered repository registers it.
269
+ projectResolution = await registerRepositoryProject(this, projectResolution);
270
+ }
271
+ projectResolution = await resolveTaskEntryProject(this, projectResolution, {
272
+ sessionId, sourceSessionId, sourceApplication, agentInvocation, agentToolName
273
+ });
262
274
  return taskEntryPreferences(this, projectResolution, {
263
275
  personalProjectId, projectAgentId, projectPath, taskPrompt, sourceApplication,
264
276
  sourceSessionId, sessionId, turnId, workKind, requiredCapabilities,
@@ -1152,7 +1164,7 @@ export class FederatedGraphApplication extends ProjectAgentControlPlaneApplicati
1152
1164
  };
1153
1165
  }
1154
1166
 
1155
- close() { this.roundtables?.close(); return this.employees?.close(); }
1167
+ close() { this.roundtable?.close(); return this.employees?.close(); }
1156
1168
 
1157
1169
  async #recordAgentViews(items, toolName) {
1158
1170
  const unique = new Map(items.map((item) => [
@@ -71,6 +71,8 @@ export function projectAgentRecord(value) {
71
71
  Object.hasOwn(profile, 'display_name'));
72
72
  copyOptional(result, 'legacyAgentIds', value.legacy_agent_ids,
73
73
  Object.hasOwn(value, 'legacy_agent_ids'));
74
+ copyOptional(result, 'employeeNumber', value.employee_number,
75
+ Object.hasOwn(value, 'employee_number'));
74
76
  copyOptional(result.profile, 'workKinds', profile.work_kinds,
75
77
  Object.hasOwn(profile, 'work_kinds'));
76
78
  copyOptional(
@@ -8,26 +8,56 @@ const ENTRY_TOOLS = new Set(['begin_task_context', 'get_collaboration_preference
8
8
 
9
9
  export async function resolveTaskEntryAgent(application, resolution, input) {
10
10
  if (!input.agentInvocation || !ENTRY_TOOLS.has(input.agentToolName)) return null;
11
- if (!resolution.personalProjectId) return { status: 'project_unresolved', worker_started: false };
11
+ if (!resolution.personalProjectId) return {
12
+ status: resolution.status === 'unavailable' ? 'unavailable' : 'project_unresolved',
13
+ reason: resolution.reason ?? resolution.status,
14
+ worker_started: false
15
+ };
12
16
  try {
13
17
  const mention = await resolveTaskAgentMention(application, resolution.personalProjectId, input);
14
18
  if (mention?.status) return mention;
15
19
  const workKind = input.workKind ?? taskWorkKind(input.taskPrompt);
16
- const value = await application.personal.resolveProjectAgentContext({
20
+ const scope = {
17
21
  personal_space_id: application.config.personal.spaceId,
18
22
  personal_project_id: resolution.personalProjectId,
23
+ source_application: input.sourceApplication ?? 'other'
24
+ };
25
+ const contextRequest = {
26
+ ...scope,
19
27
  agent_id: mention?.agentId ?? input.projectAgentId ?? null,
20
28
  session_id: input.sessionId ?? null,
21
29
  turn_id: input.turnId ?? null,
22
30
  work_kind: workKind,
23
- required_capabilities: input.requiredCapabilities ?? [],
24
- source_application: input.sourceApplication ?? 'other'
31
+ required_capabilities: input.requiredCapabilities ?? []
32
+ };
33
+ const delegation = application.roundtable?.taskEntryDelegation?.({
34
+ sessionId: input.receivingSessionId ?? input.sessionId
25
35
  });
36
+ if (delegation) {
37
+ contextRequest.delegation_token = delegation.token;
38
+ contextRequest.delegation_session_id = delegation.sessionId;
39
+ }
40
+ let value = await application.personal.resolveProjectAgentContext(contextRequest);
41
+ if (value?.reason === 'project_lead_required') {
42
+ await application.personal.staffDefaultProjectLead(scope);
43
+ // Re-resolve after staffing so a requested specialist is not lost and a
44
+ // concurrent successful hire is handled without selecting another owner.
45
+ value = await application.personal.resolveProjectAgentContext(contextRequest);
46
+ }
26
47
  if (!value?.status) throw new Error('Role resolution is unavailable');
27
- const agent = value.agent ? projectAgentRecord(value.agent) : null;
28
- if (agent && !mention?.agentId && !input.projectAgentId && unrelatedPeer(agent, workKind, input.requiredCapabilities)) {
29
- return { status: 'unassigned', reason: 'specialist_required', worker_started: false,
30
- required_action: 'Use coordinate_project_agent_task to select or recruit a qualified durable owner before implementation. HR and project managers collaborate in their own roles; do not attribute development to them by fallback.' };
48
+ let agent = value.agent ? projectAgentRecord(value.agent) : null;
49
+ const implicit = !mention?.agentId && !input.projectAgentId;
50
+ const peerOnly = agent && implicit && unrelatedPeer(agent, workKind, input.requiredCapabilities);
51
+ if (implicit && value.reason !== 'project_lead_required' && (peerOnly || value.status === 'unassigned')) {
52
+ // Every task gets a FULI Agent: a project without a lead has HR hire one.
53
+ const staffed = await application.personal.staffDefaultProjectLead(scope).catch(() => null);
54
+ if (staffed?.agent) {
55
+ value = staffed;
56
+ agent = projectAgentRecord(staffed.agent);
57
+ } else if (peerOnly) {
58
+ return { status: 'unassigned', reason: 'specialist_required', worker_started: false,
59
+ required_action: 'Use coordinate_project_agent_task to select or recruit a qualified durable owner before implementation. HR and project managers collaborate in their own roles; do not attribute development to them by fallback.' };
60
+ }
31
61
  }
32
62
  const employeeId = /^employee\.([a-z][a-z0-9-]{0,63})$/.exec(agent?.agentId ?? '')?.[1];
33
63
  if (employeeId && application.employees) {
@@ -15,7 +15,10 @@ export async function loadProjectTeamContext(application, personalSpaceId, proje
15
15
  && assignment.status === 'active')?.responsibility || agent.profile?.responsibility,
16
16
  role: agent.agent_id === policy.team_lead_agent_id ? 'lead' : 'member' }));
17
17
  return { lead_agent_id: policy.team_lead_agent_id, roster,
18
+ user_facing_agent_id: policy.team_lead_agent_id,
19
+ reporting_lines: roster.filter(agent => agent.role === 'member')
20
+ .map(agent => ({ agent_id: agent.agent_id, reports_to_agent_id: policy.team_lead_agent_id })),
18
21
  unavailable_agent_ids: ids.filter(id => !roster.some(agent => agent.agent_id === id)),
19
22
  peer_roles: ['hr', 'project_manager'], worker_started: false,
20
- guidance: 'The team lead is the user-facing owner. Split bounded work among team members through an authorized coordination plan; report real worker evidence. HR and project managers remain peers. Team membership grants no new tool, data, executor, or publication permissions.' };
23
+ guidance: 'The project lead is the sole user-facing owner. Members report results and blockers to this lead; do not skip reporting levels or replace the lead when a member is named. The lead integrates and verifies member reports before reporting to the user. Split bounded work through an authorized coordination plan; report real worker evidence. HR and project managers remain peers. Team membership grants no new tool, data, executor, or publication permissions.' };
21
24
  }
@@ -47,6 +47,8 @@ export function resolvePersonalProjectPath(projectPath, projects, {
47
47
  fileExists,
48
48
  pathApi
49
49
  });
50
+ // An unregistered repository can be registered under this exact ID.
51
+ let repositoryProjectId = null;
50
52
  if (repositoryRoot) {
51
53
  const worktreeId = worktreeOriginProjectId(
52
54
  pathApi.join(repositoryRoot, '.git'),
@@ -56,10 +58,13 @@ export function resolvePersonalProjectPath(projectPath, projects, {
56
58
  // A worktree's folder name is arbitrary; only its original repository
57
59
  // identifies the project, even when the folder names another project.
58
60
  return resolveProjectId(worktreeId, projectIds, 'git_worktree_origin')
59
- ?? unresolved('unmatched');
61
+ ?? unresolved('unmatched', safeProjectId(worktreeId, pathApi) ? { repositoryProjectId: worktreeId } : {});
60
62
  }
61
63
  const repository = resolveProjectId(pathApi.basename(repositoryRoot), projectIds, 'repository_root');
62
64
  if (repository) return repository;
65
+ if (safeProjectId(pathApi.basename(repositoryRoot), pathApi)) {
66
+ repositoryProjectId = pathApi.basename(repositoryRoot);
67
+ }
63
68
  }
64
69
 
65
70
  const directory = resolveProjectId(pathApi.basename(currentPath), projectIds, 'directory_name');
@@ -76,7 +81,7 @@ export function resolvePersonalProjectPath(projectPath, projects, {
76
81
  if (childMatches.length > 1) {
77
82
  return unresolved('ambiguous', { candidateCount: childMatches.length });
78
83
  }
79
- return unresolved('unmatched');
84
+ return unresolved('unmatched', repositoryProjectId ? { repositoryProjectId } : {});
80
85
  }
81
86
 
82
87
  function resolveProjectId(candidate, projectIds, basis) {
@@ -153,6 +153,23 @@ export class GraphitiProviderClient {
153
153
  resolveProjectAgentContext(input) {
154
154
  return this.#request('/v1/project-agent-context/resolve', { method: 'POST', body: input });
155
155
  }
156
+ issueAgentDelegation(input) {
157
+ return this.#request('/v1/project-agent-context/delegations', { method: 'POST', body: input });
158
+ }
159
+ verifyAgentDelegation(input) {
160
+ return this.#request('/v1/project-agent-context/delegations/verify', { method: 'POST', body: input });
161
+ }
162
+ revokeAgentDelegation(input) {
163
+ return this.#request('/v1/project-agent-context/delegations/revoke', { method: 'POST', body: input });
164
+ }
165
+ recentAgentSessions({ personalSpaceId, agentId, personalProjectId = null, limit = 5 }) {
166
+ const query = new URLSearchParams({ personal_space_id: personalSpaceId, project_agent_id: agentId, limit: String(limit) });
167
+ if (personalProjectId) query.set('personal_project_id', personalProjectId);
168
+ return this.#request(`/v1/task-context-sessions/recent?${query}`);
169
+ }
170
+ staffDefaultProjectLead(input) {
171
+ return this.#request('/v1/project-agent-context/default-lead', { method: 'POST', body: input });
172
+ }
156
173
  beginTaskContext(input) {
157
174
  return this.#request('/v1/task-contexts', { method: 'PUT', body: input });
158
175
  }
@@ -0,0 +1,17 @@
1
+ export const TASK_ENTRY_TOOLS = new Set(['begin_task_context', 'get_collaboration_preferences']);
2
+
3
+ // Registers the exact repository ID the path resolver would match next time.
4
+ // A failed registration leaves the task unresolved rather than guessing.
5
+ export async function registerRepositoryProject(application, resolution) {
6
+ const projectId = resolution.repositoryProjectId;
7
+ try {
8
+ await application.upsertPersonalProject({
9
+ personalSpaceId: application.config.personal.spaceId,
10
+ projectId,
11
+ profile: { name: projectId, lifecycle: 'active' }
12
+ });
13
+ } catch {
14
+ return resolution;
15
+ }
16
+ return { status: 'matched', basis: 'registered_repository', personalProjectId: projectId };
17
+ }
@@ -7,12 +7,11 @@ export async function taskEntryPreferences(application, projectResolution, {
7
7
  sourceSessionId, sessionId, turnId, workKind, requiredCapabilities,
8
8
  limit, agentInvocation, agentToolName
9
9
  }, recordAgentViews) {
10
- const selection = projectPath === null && !personalProjectId && !projectAgentId
11
- ? null
12
- : await resolveTaskEntryAgent(
10
+ const selection = await resolveTaskEntryAgent(
13
11
  application, projectResolution, { projectAgentId, taskPrompt, sourceApplication,
14
12
  sessionId: agentToolName === 'get_collaboration_preferences'
15
13
  ? sessionId ?? sourceSessionId : turnId ? sessionId : null,
14
+ receivingSessionId: sessionId ?? sourceSessionId,
16
15
  turnId: agentToolName === 'begin_task_context' ? turnId : null,
17
16
  workKind, requiredCapabilities, agentInvocation, agentToolName }
18
17
  );
@@ -41,9 +40,23 @@ export async function taskEntryPreferences(application, projectResolution, {
41
40
  sourceApplication, taskPrompt, selectionReason: selection.reason,
42
41
  matchBasis: selection.match_basis
43
42
  }) : selection;
43
+ if (context && selection?.requested_agent_id) {
44
+ context.requested_agent_id = selection.requested_agent_id;
45
+ context.requested_agent_guidance = 'The user requested this project specialist. Keep the project lead as accountable owner; coordinate a scoped workstream for the specialist and receive its report before replying. This request is not evidence that a worker ran. Do not load the specialist’s private memory into the lead context.';
46
+ }
47
+ if (context && selection?.reporting_lead_agent_id) {
48
+ context.reporting_lead_agent_id = selection.reporting_lead_agent_id;
49
+ context.reporting_guidance = 'This is a delegated member turn. Keep this member identity and private memory isolated. Return results and blockers to the project lead; the lead integrates the final user report.';
50
+ }
51
+ // Questions other Agents left for this Agent while it was not running.
52
+ const waiting = selection?.agent && application.roundtable
53
+ ? application.roundtable.pending(selection.agent.agentId) : [];
44
54
  return { ...managedPreferences, ...(context ? { project_agent_context: context } : {}),
55
+ ...(waiting.length ? { agent_messages: waiting,
56
+ agent_messages_guidance: 'Other Agents are waiting for your answers. Reply to each with reply_agent_message.' } : {}),
45
57
  agent_receipt: taskAgentReceipt(application, {
46
58
  projectId: projectResolution.personalProjectId, agentId: selection?.agent?.agentId ?? null,
47
- name: context?.role?.name, sourceApplication
59
+ name: context?.role?.name, sourceApplication,
60
+ assignmentStatus: selection?.status, assignmentReason: selection?.reason
48
61
  }) };
49
62
  }