fuli-context 0.11.0 → 0.12.0

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 (169) hide show
  1. package/dist/web/assets/AboutPage-BAD0iPVX.js +1 -0
  2. package/dist/web/assets/AgentConversations-1iGTd4sz.js +2 -0
  3. package/dist/web/assets/AgentHand-D_RafKOM.js +1 -0
  4. package/dist/web/assets/AgentName-B5LKQ09q.js +1 -0
  5. package/dist/web/assets/AgentProfilePage-CioOdkBM.js +1 -0
  6. package/dist/web/assets/AgentsDirectoryPage--1A9nvNA.js +1 -0
  7. package/dist/web/assets/AgentsDirectoryPage-C7OUQH9v.css +1 -0
  8. package/dist/web/assets/ConnectionsPage-Tr2rRVOZ.js +1 -0
  9. package/dist/web/assets/EmployeeRecruitDialog-1QevD4Qv.js +1 -0
  10. package/dist/web/assets/EmployeeWorkbenchPage-B0vQIgwA.js +3 -0
  11. package/dist/web/assets/EmployeeWorkbenchPage-BQes4L1f.css +1 -0
  12. package/dist/web/assets/KnowledgeEditDialog-ClnrfPZi.js +1 -0
  13. package/dist/web/assets/KnowledgeInspector-Te1hJZbx.js +1 -0
  14. package/dist/web/assets/KnowledgeOrganizerPage-CCCrWl6Y.js +1 -0
  15. package/dist/web/assets/KnowledgeOrganizerPage-ccgscrPd.css +1 -0
  16. package/dist/web/assets/KnowledgePage-DGOcBwr8.js +1 -0
  17. package/dist/web/assets/KnowledgeWorkspace-cKn1NCRe.js +4 -0
  18. package/dist/web/assets/NotFoundPage-fetSnp0N.js +1 -0
  19. package/dist/web/assets/OverviewPage-BD4yY2Xz.js +1 -0
  20. package/dist/web/assets/PersonalProfilePage-DGJVP_VB.js +1 -0
  21. package/dist/web/assets/PersonalProjectsPage-D25HYMgD.js +1 -0
  22. package/dist/web/assets/ProjectAgentFirstTask-BcxqmKXQ.js +1 -0
  23. package/dist/web/assets/ProjectAgentsPage-BWDJBESb.js +12 -0
  24. package/dist/web/assets/{ProjectAgentsPage-DNLNjYp-.css → ProjectAgentsPage-DWGkgIsr.css} +1 -1
  25. package/dist/web/assets/ProjectScopePicker-yodM7lF_.js +1 -0
  26. package/dist/web/assets/PublicProjectsPage-CyNxVzW3.js +1 -0
  27. package/dist/web/assets/ReviewPage-Cy0Rt4CO.js +1 -0
  28. package/dist/web/assets/RoundtablesPage-BjVhvwMc.js +1 -0
  29. package/dist/web/assets/SettingsPage-CCZqZtnH.js +1 -0
  30. package/dist/web/assets/SettingsPage-DYz5bTwa.css +1 -0
  31. package/dist/web/assets/SharedJefaReportPage-C56kPciB.js +1 -0
  32. package/dist/web/assets/UiDialog-CvDxg3B1.js +1 -0
  33. package/dist/web/assets/UiDisclosure-D0iMTplF.js +1 -0
  34. package/dist/web/assets/UiSegmented-DIJW6wv3.js +1 -0
  35. package/dist/web/assets/UiSelect-CMR0QqF6.js +1 -0
  36. package/dist/web/assets/WritingTastePage-QgNzssfF.js +1 -0
  37. package/dist/web/assets/agent-pins-BcJ_qr8n.js +1 -0
  38. package/dist/web/assets/agent-work-summary-C-yW7x7A.js +1 -0
  39. package/dist/web/assets/{attention-store-BfxtL9EV.js → attention-store-B1DI8wmd.js} +1 -1
  40. package/dist/web/assets/{catalog-6SN9i3WR.js → catalog-DbXv0_bR.js} +1 -1
  41. package/dist/web/assets/client-CR-W5Wzp.js +1 -0
  42. package/dist/web/assets/console-D7GHOMEC.js +1 -0
  43. package/dist/web/assets/conversation-launcher-settings-Dj2GMzCI.js +1 -0
  44. package/dist/web/assets/{i18n-CCMR4gDS.js → i18n-CVFhT_k7.js} +3 -3
  45. package/dist/web/assets/{identity-C1eN-FSB.js → identity-CM6Dmy00.js} +1 -1
  46. package/dist/web/assets/index-CJ6Jmok9.css +1 -0
  47. package/dist/web/assets/index-CRa3GnYi.js +5 -0
  48. package/dist/web/assets/judgment-types-CJBEyJs1.js +1 -0
  49. package/dist/web/assets/model-WFhQZ3v_.js +1 -0
  50. package/dist/web/assets/{personal-profile-graph-BD1Y6TDS.js → personal-profile-graph-BwCh2M9G.js} +1 -1
  51. package/dist/web/assets/shared-report-CeIvrL9a.js +1 -0
  52. package/dist/web/assets/{agent-work-summary-LeW-Al_S.js → task-evidence-DTiPCs7r.js} +1 -1
  53. package/dist/web/assets/useAgentRoster-DCArHlxK.js +1 -0
  54. package/dist/web/assets/{useApi-CROJJdhE-DlOiu8lP.js → useApi-CROJJdhE-BYhbjaZB.js} +1 -1
  55. package/dist/web/assets/vue-router-DaxrQwX3.js +1 -0
  56. package/dist/web/index.html +16 -12
  57. package/docs/agent-roundtable.md +17 -1
  58. package/docs/employee-agents.md +1 -1
  59. package/docs/judgment-and-autonomy.md +33 -0
  60. package/graph-provider/fuli_graph/ai_review.py +154 -0
  61. package/graph-provider/fuli_graph/common_knowledge.py +12 -0
  62. package/graph-provider/fuli_graph/graph_models.py +14 -0
  63. package/graph-provider/fuli_graph/graph_query.py +217 -0
  64. package/graph-provider/fuli_graph/knowledge_audit.py +6 -0
  65. package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +6 -0
  66. package/graph-provider/fuli_graph/knowledge_management.py +23 -0
  67. package/graph-provider/fuli_graph/knowledge_review.py +383 -4
  68. package/graph-provider/fuli_graph/knowledge_review_models.py +61 -2
  69. package/graph-provider/fuli_graph/personal_global_preferences.py +5 -1
  70. package/graph-provider/fuli_graph/preference_conflicts.py +91 -1
  71. package/graph-provider/fuli_graph/project_agent_context_models.py +5 -1
  72. package/graph-provider/fuli_graph/project_agent_delegations.py +118 -0
  73. package/graph-provider/fuli_graph/project_agent_employee_numbers.py +41 -0
  74. package/graph-provider/fuli_graph/project_agent_memory_routes.py +13 -0
  75. package/graph-provider/fuli_graph/project_agent_models.py +1 -0
  76. package/graph-provider/fuli_graph/project_agent_task_models.py +2 -0
  77. package/graph-provider/fuli_graph/project_agent_team_growth.py +2 -2
  78. package/graph-provider/fuli_graph/project_knowledge.py +41 -0
  79. package/graph-provider/fuli_graph/runtime.py +15 -0
  80. package/graph-provider/fuli_graph/store_agent_verification.py +12 -5
  81. package/graph-provider/fuli_graph/store_knowledge.py +98 -58
  82. package/graph-provider/fuli_graph/store_project_agent_attention.py +1 -1
  83. package/graph-provider/fuli_graph/store_project_agent_context.py +52 -4
  84. package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +16 -1
  85. package/graph-provider/fuli_graph/store_project_agent_task_activity.py +26 -0
  86. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +5 -3
  87. package/graph-provider/fuli_graph/store_project_agents.py +31 -5
  88. package/graph-provider/fuli_graph/system_agents.py +23 -2
  89. package/graph-provider/fuli_graph/system_hr_identity.py +19 -5
  90. package/graph-provider/pyproject.toml +1 -1
  91. package/npm-shrinkwrap.json +2 -2
  92. package/package.json +3 -2
  93. package/src/agent-roundtable/service.js +34 -5
  94. package/src/agent-roundtable/wake.js +2 -1
  95. package/src/agent-tools/graph-definitions.js +2 -0
  96. package/src/agent-tools/graph-handlers.js +7 -0
  97. package/src/agent-tools/interface-catalog.js +6 -0
  98. package/src/agent-tools/judgment-definitions.js +12 -0
  99. package/src/agents/claude-code/wake.js +7 -0
  100. package/src/agents/codex/wake.js +7 -0
  101. package/src/agents/identity-receipt.js +20 -2
  102. package/src/employees/catalog/tonborg.json +20 -0
  103. package/src/employees/manifest.js +2 -2
  104. package/src/employees/recruitment.js +20 -1
  105. package/src/graphiti/agent-collaboration.js +8 -2
  106. package/src/graphiti/federated-application.js +11 -2
  107. package/src/graphiti/knowledge-review.js +7 -3
  108. package/src/graphiti/project-agent-mapping.js +4 -0
  109. package/src/graphiti/project-agent-task-entry.js +23 -5
  110. package/src/graphiti/project-agent-team-context.js +4 -1
  111. package/src/graphiti/provider-client.js +9 -0
  112. package/src/graphiti/task-entry-preferences.js +17 -4
  113. package/src/graphiti/task-entry-project-scope.js +67 -0
  114. package/src/http/api-router.js +2 -0
  115. package/src/judgment/acceptance.js +91 -0
  116. package/src/judgment/engine.js +168 -0
  117. package/src/judgment/http.js +24 -0
  118. package/src/judgment/model.js +24 -0
  119. package/src/judgment/routing.js +96 -0
  120. package/src/judgment/scheduler.js +25 -0
  121. package/src/judgment/service.js +66 -0
  122. package/src/judgment/store.js +123 -0
  123. package/src/mcp/session-id.js +1 -0
  124. package/src/mcp/tool-annotations.js +4 -2
  125. package/src/server.js +9 -2
  126. package/dist/web/assets/AboutPage-CKq4IRHd.js +0 -1
  127. package/dist/web/assets/AgentConversations-DfXiFvBN.js +0 -2
  128. package/dist/web/assets/AgentHand-BBM-sKh2.js +0 -1
  129. package/dist/web/assets/AgentName-C_6R80nP.js +0 -1
  130. package/dist/web/assets/AgentProfilePage-Cz-i1fRD.js +0 -1
  131. package/dist/web/assets/AgentsDirectoryPage-BI0EFz6y.js +0 -1
  132. package/dist/web/assets/AgentsDirectoryPage-yRWktiin.css +0 -1
  133. package/dist/web/assets/ConnectionsPage-BoC6kXgs.js +0 -1
  134. package/dist/web/assets/EmployeeRecruitDialog-BVyclQAJ.js +0 -1
  135. package/dist/web/assets/EmployeeWorkbenchPage-D-c_Jm2z.css +0 -1
  136. package/dist/web/assets/EmployeeWorkbenchPage-D2GvYraI.js +0 -3
  137. package/dist/web/assets/KnowledgeEditDialog-a0AKWics.js +0 -1
  138. package/dist/web/assets/KnowledgeInspector-DMg9yJZ2.js +0 -1
  139. package/dist/web/assets/KnowledgeOrganizerPage-BgsHM3Qw.js +0 -1
  140. package/dist/web/assets/KnowledgeOrganizerPage-DeL7Jp34.css +0 -1
  141. package/dist/web/assets/KnowledgePage-kn8MqUCO.js +0 -1
  142. package/dist/web/assets/KnowledgeWorkspace-BtRq9fvC.js +0 -4
  143. package/dist/web/assets/NotFoundPage-D-7jLz3o.js +0 -1
  144. package/dist/web/assets/OverviewPage-BHPc-XxY.js +0 -1
  145. package/dist/web/assets/PersonalProfilePage-C00iV0Kx.js +0 -1
  146. package/dist/web/assets/PersonalProjectsPage-CFNkdHSo.js +0 -1
  147. package/dist/web/assets/ProjectAgentFirstTask-CrCWEYgn.js +0 -1
  148. package/dist/web/assets/ProjectAgentsPage-BzztLMH-.js +0 -12
  149. package/dist/web/assets/ProjectScopePicker-CMxn3txv.js +0 -1
  150. package/dist/web/assets/PublicProjectsPage-DQVtvhOS.js +0 -1
  151. package/dist/web/assets/ReviewPage-BZhvyDWc.js +0 -1
  152. package/dist/web/assets/RoundtablesPage-CVebTXQa.js +0 -1
  153. package/dist/web/assets/SettingsPage-1knvfOW6.js +0 -1
  154. package/dist/web/assets/SettingsPage-BmYkLul4.css +0 -1
  155. package/dist/web/assets/SharedJefaReportPage-BCVq8u0P.js +0 -1
  156. package/dist/web/assets/UiDialog-Da1P10f4.js +0 -1
  157. package/dist/web/assets/UiDisclosure-8wE8YuE4.js +0 -1
  158. package/dist/web/assets/UiSegmented-BKZXc8QF.js +0 -1
  159. package/dist/web/assets/UiSelect-D5Xj5Ec4.js +0 -1
  160. package/dist/web/assets/WritingTastePage-B8-etOBn.js +0 -1
  161. package/dist/web/assets/client-CUTkF3t4.js +0 -1
  162. package/dist/web/assets/console-CG8tdtSs.js +0 -1
  163. package/dist/web/assets/conversation-launcher-settings-CyZkLJg7.js +0 -1
  164. package/dist/web/assets/index-kPB0Cf8Q.css +0 -1
  165. package/dist/web/assets/index-sLDKI6R-.js +0 -5
  166. package/dist/web/assets/model-l-sAqtW_.js +0 -1
  167. package/dist/web/assets/shared-report-B46dxlCR.js +0 -1
  168. package/dist/web/assets/useAgentRoster-DdxoKDEE.js +0 -1
  169. package/dist/web/assets/vue-router-DjqEU4W2.js +0 -1
@@ -149,17 +149,31 @@ async def merge_legacy_hr(store, space_id):
149
149
  '''MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
150
150
  MATCH (n) WHERE n.group_id = space.group_id
151
151
  AND (n:Entity OR n:Episodic)
152
- FOREACH (_ IN CASE WHEN n.fuli_project_agent_id = $legacy_id THEN [1] ELSE [] END |
153
- SET n.fuli_project_agent_id = $canonical_id, n.hr_identity_original_agent_id = $legacy_id)
154
- FOREACH (_ IN CASE WHEN n.fuli_preference_agent_id = $legacy_id THEN [1] ELSE [] END |
155
- SET n.fuli_preference_agent_id = $canonical_id, n.hr_identity_original_agent_id = $legacy_id)''',
152
+ AND (n.fuli_project_agent_id = $legacy_id
153
+ OR n.fuli_preference_agent_id = $legacy_id)
154
+ WITH n
155
+ ORDER BY CASE WHEN n:Entity THEN 'entity:' ELSE 'episodic:' END +
156
+ coalesce(n.uuid, '')
157
+ SET n.fuli_ai_review_lock_version =
158
+ coalesce(n.fuli_ai_review_lock_version, 0) + 1,
159
+ n.fuli_project_agent_id = CASE
160
+ WHEN n.fuli_project_agent_id = $legacy_id
161
+ THEN $canonical_id ELSE n.fuli_project_agent_id END,
162
+ n.fuli_preference_agent_id = CASE
163
+ WHEN n.fuli_preference_agent_id = $legacy_id
164
+ THEN $canonical_id ELSE n.fuli_preference_agent_id END,
165
+ n.hr_identity_original_agent_id = $legacy_id''',
156
166
  **scope,
157
167
  )
158
168
  await driver.execute_query(
159
169
  '''MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
160
170
  MATCH ()-[edge:RELATES_TO]->() WHERE edge.group_id = space.group_id
161
171
  AND edge.fuli_preference_agent_id = $legacy_id
162
- SET edge.fuli_preference_agent_id = $canonical_id,
172
+ WITH edge
173
+ ORDER BY 'relationship:' + coalesce(edge.uuid, '')
174
+ SET edge.fuli_ai_review_lock_version =
175
+ coalesce(edge.fuli_ai_review_lock_version, 0) + 1,
176
+ edge.fuli_preference_agent_id = $canonical_id,
163
177
  edge.hr_identity_original_agent_id = $legacy_id''', **scope,
164
178
  )
165
179
  await driver.execute_query(
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fuli-graph-provider"
3
- version = "0.11.0"
3
+ version = "0.12.0"
4
4
  description = "Graphiti-backed personal and shared project provider for Fuli."
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "fuli-context",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "fuli-context",
9
- "version": "0.11.0",
9
+ "version": "0.12.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@a2a-js/sdk": "1.3.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fuli-context",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Local-first collaboration relationship graph for AI agents.",
5
5
  "type": "module",
6
6
  "files": [
@@ -21,7 +21,8 @@
21
21
  "graph-provider/fuli_graph/*.py",
22
22
  "graph-provider/fuli_graph/**/*.py",
23
23
  "skills/",
24
- "compose.graphiti.yml"
24
+ "compose.graphiti.yml",
25
+ "docs/judgment-and-autonomy.md"
25
26
  ],
26
27
  "bin": {
27
28
  "fl": "src/cli.js",
@@ -10,7 +10,7 @@ const NESTED_ASK_LIMIT = 3;
10
10
  // Agents talk to each other directly: an ask wakes the recipient in its own
11
11
  // client and returns its answer, or waits in its inbox for its next task.
12
12
  export function createAgentRoundtable({ app, openStore, wake = wakeAgent, idFactory = randomUUID,
13
- locateCwd = sessionWorkingDirectory, clientAvailable = (client) => Boolean(resolveClientCommand(client)) }) {
13
+ env = process.env, locateCwd = sessionWorkingDirectory, clientAvailable = (client) => Boolean(resolveClientCommand(client)) }) {
14
14
  const spaceId = () => app.config.personal.spaceId;
15
15
  // Opened on first use: most MCP processes never touch the roundtable.
16
16
  let opened = null;
@@ -35,6 +35,9 @@ export function createAgentRoundtable({ app, openStore, wake = wakeAgent, idFact
35
35
  const all = await agents();
36
36
  const exact = all.find((agent) => agent.agentId === needle || agent.agentId === decodeURIComponent(needle));
37
37
  if (exact) return exact;
38
+ const number = needle.match(/^(?:FLA\s*)?(\d{6,})$/i)?.[1];
39
+ const numbered = number && all.find((agent) => agent.employeeNumber === number);
40
+ if (numbered) return numbered;
38
41
  const named = all.filter((agent) => [agent.profile.displayName, agent.profile.name]
39
42
  .some((name) => name?.toLowerCase() === needle.toLowerCase()));
40
43
  if (named.length === 1) return named[0];
@@ -53,11 +56,12 @@ export function createAgentRoundtable({ app, openStore, wake = wakeAgent, idFact
53
56
 
54
57
  async function findAgents({ query = null, personalProjectId = null } = {}) {
55
58
  const needle = query?.trim().toLowerCase();
56
- const matches = (await agents()).filter((agent) => !needle || [agent.agentId, agent.profile.displayName,
59
+ const matches = (await agents()).filter((agent) => !needle || [agent.agentId,
60
+ agent.employeeNumber ? `FLA ${agent.employeeNumber}` : null, agent.profile.displayName,
57
61
  agent.profile.name, agent.profile.responsibility, ...(agent.profile.capabilities ?? [])]
58
62
  .some((value) => value?.toLowerCase().includes(needle)));
59
63
  return { agents: await Promise.all(matches.slice(0, 20).map(async (agent) => ({
60
- agentId: agent.agentId, name: label(agent), responsibility: agent.profile.responsibility,
64
+ agentId: agent.agentId, employeeNumber: agent.employeeNumber, name: label(agent), responsibility: agent.profile.responsibility,
61
65
  clients: (agent.profile.allowedClients ?? []).filter((client) => WAKE_CLIENTS.includes(client)),
62
66
  recentConversations: await conversations(agent, personalProjectId)
63
67
  }))),
@@ -107,6 +111,7 @@ export function createAgentRoundtable({ app, openStore, wake = wakeAgent, idFact
107
111
  to: { agentId: target.agentId, name: label(target) } });
108
112
  const nested = history.filter((message) => message.kind === 'ask' && message.status === 'delivering').length;
109
113
  if (!wait || nested >= NESTED_ASK_LIMIT) return queued(thread, ask, target, nested ? 'nested_limit' : 'not_waiting');
114
+ if (!taskContextToken || !from.projectId) return queued(thread, ask, target, 'task_context_required');
110
115
  const attempts = await deliveryTargets(target, conversation, from.projectId, projectPath);
111
116
  if (!attempts.length) return queued(thread, ask, target, 'no_client');
112
117
  const prompt = wakePrompt({ thread, ask, target, senderName: ask.from_name, senderClient: from.client });
@@ -117,8 +122,20 @@ export function createAgentRoundtable({ app, openStore, wake = wakeAgent, idFact
117
122
  if (unusable.has(delivery.client)) continue;
118
123
  store.updateMessage(ask.id, { status: 'delivering', toClient: delivery.client, toSession: delivery.sessionId });
119
124
  let answer;
125
+ let grant;
120
126
  try {
121
- answer = await wake({ client: delivery.client, sessionId: delivery.sessionId, cwd: delivery.cwd, timeoutMs, prompt });
127
+ grant = await app.personal.issueAgentDelegation({ personal_space_id: spaceId(),
128
+ task_context_token: taskContextToken, source_application: from.client, agent_id: target.agentId,
129
+ target_application: delivery.client,
130
+ // Claude resumes into a fork with a new ID. Only Codex resumes in place.
131
+ target_session_id: delivery.client === 'codex' ? delivery.sessionId : null,
132
+ lifetime_seconds: timeoutMs / 1000 });
133
+ answer = await wake({ client: delivery.client, sessionId: delivery.sessionId, cwd: delivery.cwd,
134
+ timeoutMs, prompt, delegationToken: grant.token });
135
+ const verified = answer.sessionId && await app.personal.verifyAgentDelegation({
136
+ personal_space_id: spaceId(), token: grant.token, session_id: answer.sessionId });
137
+ if (!verified?.redeemed) throw Object.assign(new Error('The receiving session did not restore the Agent'),
138
+ { code: 'delegation_not_redeemed' });
122
139
  } catch (error) {
123
140
  failure = error.code ?? 'wake_failed';
124
141
  // A missing or logged-out client cannot answer from any session. A conversation
@@ -127,6 +144,11 @@ export function createAgentRoundtable({ app, openStore, wake = wakeAgent, idFact
127
144
  // A timeout already used the caller's wait.
128
145
  if (failure === 'wake_timeout') break;
129
146
  continue;
147
+ } finally {
148
+ // Even failed or timed-out attempts lose their authority. Expiry remains
149
+ // the fail-closed backstop if the Provider is temporarily unreachable.
150
+ if (grant?.token) await app.personal.revokeAgentDelegation({
151
+ personal_space_id: spaceId(), token: grant.token }).catch(() => {});
130
152
  }
131
153
  const via = `${delivery.client}:${delivery.sessionId ? 'resume' : 'new'}`;
132
154
  store.updateMessage(ask.id, { status: 'answered', via });
@@ -187,7 +209,14 @@ export function createAgentRoundtable({ app, openStore, wake = wakeAgent, idFact
187
209
  to: { agentId: message.to_agent, name: message.to_name, client: message.to_client } })) };
188
210
  }
189
211
 
190
- return { findAgents, messageAgent, readMessages, replyMessage, threads, thread,
212
+ function taskEntryDelegation({ sessionId }) {
213
+ // The public thread header is presentation only. The Provider validates this
214
+ // process-local capability, including project, recipient, client and session.
215
+ return env.FULI_ROUNDTABLE_DELEGATION
216
+ ? { token: env.FULI_ROUNDTABLE_DELEGATION, sessionId: sessionId ?? null } : null;
217
+ }
218
+
219
+ return { findAgents, messageAgent, readMessages, replyMessage, threads, thread, taskEntryDelegation,
191
220
  pending: (agentId) => store.pendingFor(spaceId(), agentId).map(inboxItem),
192
221
  close: () => opened?.close() };
193
222
  }
@@ -33,10 +33,11 @@ export function sessionWorkingDirectory(client, sessionId, { home = homedir(), e
33
33
  }
34
34
 
35
35
  export async function wakeAgent({ client, sessionId = null, cwd, prompt, timeoutMs = 300_000,
36
- env = process.env, run = runProcess } = {}) {
36
+ delegationToken = null, env = process.env, run = runProcess } = {}) {
37
37
  const command = resolveClientCommand(client, { env });
38
38
  if (!command) throw Object.assign(new Error(`${client} is not installed on this machine`), { code: 'client_unavailable' });
39
39
  const childEnv = Object.fromEntries(Object.entries(env).filter(([key]) => !key.startsWith('FULI_ROUNDTABLE_')));
40
+ if (delegationToken) childEnv.FULI_ROUNDTABLE_DELEGATION = delegationToken;
40
41
  const result = await run(command, wakeArguments(client, { sessionId, cwd }), { cwd, env: childEnv, input: prompt, timeoutMs });
41
42
  return parseFinalAnswer(client, result);
42
43
  }
@@ -36,6 +36,7 @@ import {
36
36
  } from './project-agent-definitions.js';
37
37
  import { PROJECT_AGENT_MEMORY_DEFINITIONS, workingMemorySchema } from './project-agent-memory-definitions.js';
38
38
  import { EMPLOYEE_TOOL_DEFINITIONS } from './employee-definitions.js';
39
+ import { JUDGMENT_TOOL_DEFINITIONS } from './judgment-definitions.js';
39
40
  import { AGENT_INTERFACE_DEFINITIONS } from './interface-definitions.js';
40
41
  import { AGENT_ATTENTION_DEFINITIONS } from './agent-attention-definitions.js';
41
42
 
@@ -287,6 +288,7 @@ export const GRAPH_TOOL_DEFINITIONS = [
287
288
  ...AGENT_ATTENTION_DEFINITIONS,
288
289
  ...AGENT_INTERFACE_DEFINITIONS,
289
290
  ...EMPLOYEE_TOOL_DEFINITIONS,
291
+ ...JUDGMENT_TOOL_DEFINITIONS,
290
292
  {
291
293
  name: 'begin_task_context',
292
294
  title: 'LIFECYCLE · Begin a Fuli-aware task',
@@ -4,6 +4,13 @@ import { activeTestToolsEnabled } from '../app/agent-request-context.js';
4
4
  import { agentInterfaceCatalog } from './interface-catalog.js';
5
5
 
6
6
  const HANDLERS = Object.freeze({
7
+ get_judgment_policy: (app, input) => app.judgment.policy(input),
8
+ list_judgments: (app, input) => app.judgment.records(input),
9
+ review_with_tonborg: (app, input) => app.judgment.review(input),
10
+ assess_agent_action: (app, input) => app.judgment.assess(input),
11
+ assess_task_completion: (app, input) => app.judgment.accept(input),
12
+ get_agent_pins: (app, input) => app.judgment.pins(input),
13
+ pin_agent: (app, input) => app.judgment.pin(input),
7
14
  get_agent_quality_gate: (app, input) => app.agentVerification('query', input),
8
15
  record_agent_verification: (app, input) => app.agentVerification('record', input),
9
16
  plan_agent_collaboration: (app, input) => app.planAgentCollaboration(input),
@@ -1,4 +1,10 @@
1
1
  const UI_MUTATION_PARITY = Object.freeze([
2
+ agent('PUT /api/agent-pins', 'pin_agent'),
3
+ local('PUT /api/judgment/policy', 'Autonomy grants delegated authority and requires the local user.'),
4
+ local('POST /api/judgment/feedback', 'Votes and reasons are human feedback, never model-generated approval.'),
5
+ agent('POST /api/judgment/review', 'review_with_tonborg'),
6
+ agent('POST /api/judgment/assess', 'assess_agent_action'),
7
+ agent('POST /api/judgment/accept', 'assess_task_completion'),
2
8
  agent('POST /api/agent-conversations/query', 'list_agent_conversations', 'read_agent_conversation', 'get_agent_conversation_policy'),
3
9
  agent('PUT /api/agent-conversations/policy', 'update_agent_conversation_policy'),
4
10
  agent('POST /api/agent-attention', 'request_agent_attention'),
@@ -0,0 +1,12 @@
1
+ import { objectSchema, stringSchema, integerSchema, booleanSchema } from './schema.js';
2
+ const id = { ...stringSchema(), minLength: 1, maxLength: 256 };
3
+ const scope = { personalSpaceId: id, personalProjectId: { ...id, type: ['string', 'null'] } };
4
+ export const JUDGMENT_TOOL_DEFINITIONS = [
5
+ { name: 'get_judgment_policy', title: 'READ · Tonborg autonomy', description: 'Read effective autonomy and quality preferences, including project inheritance. Only the user changes autonomy in Settings. Never infer permission from a recommendation.', inputSchema: objectSchema(scope, ['personalSpaceId']) },
6
+ { name: 'list_judgments', title: 'READ · Tonborg decisions', description: 'Read scoped decisions, execution receipts and human feedback. A recommendation is not evidence of execution. Feedback is scoped evidence, not a new global instruction.', inputSchema: objectSchema({ ...scope, limit: integerSchema({ minimum: 1, maximum: 100 }) }, ['personalSpaceId']) },
7
+ { name: 'review_with_tonborg', title: 'WRITE · Tonborg review', description: 'Run a bounded real-model review under the saved autonomy policy. Manual mode only analyzes. AI-reviewed never asserts human confirmation. Reuse requestId on retries; failures and stale evidence remain visible. May invoke the locally configured judgment client.', inputSchema: objectSchema({ ...scope, requestId: id, limit: integerSchema({ minimum: 1, maximum: 10 }) }, ['personalSpaceId', 'requestId']) },
8
+ { name: 'assess_agent_action', title: 'READ · Tonborg executor judgment', description: 'Before a new subagent, session or client choice, ask Tonborg whether to continue, delegate, reuse a verified conversation or open a new session, and select from authorized, preflighted executors and their available models using scoped preferences and feedback. Respect locked Agent policies and user rules. The host still validates and starts workers. Report actual runtime separately; this does not grant permission or claim execution.', inputSchema: objectSchema({ ...scope, requestId: id, objective: { ...stringSchema(), minLength: 1, maxLength: 8000 }, agentId: id, workKind: id, taskId: id, requiredCapabilities: { type: 'array', items: id, maxItems: 32 }, action: { type: 'string', enum: ['subagent', 'session', 'client'] } }, ['personalSpaceId', 'personalProjectId', 'requestId', 'objective', 'agentId', 'workKind']) },
9
+ { name: 'assess_task_completion', title: 'WRITE · Tonborg task acceptance', description: 'The accountable lead asks Tonborg to assess an awaiting-review FULI task at its exact revision. Saved autonomy may complete it only with a passed real verification by another Agent. Preserves Jefa human acceptance and never approves publication or new permissions. Reuse requestId on retries; inspect the actual execution receipt.', inputSchema: objectSchema({ ...scope, requestId: id, taskContextToken: id, taskId: id, artifactRevision: { ...id, maxLength: 160 }, expectedRevision: integerSchema({ minimum: 0 }) }, ['personalSpaceId', 'personalProjectId', 'requestId', 'taskContextToken', 'taskId', 'artifactRevision', 'expectedRevision']) },
10
+ { name: 'get_agent_pins', title: 'READ · Pinned Agents', description: 'Read the user’s pinned Agent IDs and revision. An explicit empty list remains empty.', inputSchema: objectSchema({ personalSpaceId: id }, ['personalSpaceId']) },
11
+ { name: 'pin_agent', title: 'WRITE · Pin an Agent', description: 'On the user’s request pin or unpin an existing Agent in navigation. Preserve the current revision and reload after conflicts.', inputSchema: objectSchema({ personalSpaceId: id, agentId: id, pinned: booleanSchema(), expectedRevision: integerSchema({ minimum: 0 }) }, ['personalSpaceId', 'agentId', 'pinned', 'expectedRevision']) },
12
+ ];
@@ -8,6 +8,13 @@ import { firstField, listDirectory } from '../session-log.js';
8
8
  export const claudeCodeWake = Object.freeze({
9
9
  id: 'claude_code',
10
10
 
11
+ judgmentArgs({ quality }) {
12
+ return ['-p', '--output-format', 'stream-json', '--verbose', '--safe-mode',
13
+ '--tools', '', '--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}',
14
+ '--setting-sources', '', '--no-session-persistence', '--permission-mode', 'dontAsk',
15
+ '--effort', quality === 'economy' ? 'low' : quality === 'balanced' ? 'medium' : 'high'];
16
+ },
17
+
11
18
  resolveCommand({ env, which, platform }) {
12
19
  return env.FULI_CLAUDE_BIN || which('claude', platform);
13
20
  },
@@ -9,6 +9,13 @@ import { findFile, firstField, listDirectory } from '../session-log.js';
9
9
  export const codexWake = Object.freeze({
10
10
  id: 'codex',
11
11
 
12
+ judgmentArgs({ cwd, quality }) {
13
+ return ['exec', '--json', '--ephemeral', '--ignore-user-config', '--ignore-rules',
14
+ '--skip-git-repo-check', '--sandbox', 'read-only', '-C', cwd,
15
+ '-c', 'project_doc_max_bytes=0', '-c', 'features.shell_tool=false',
16
+ '-c', `model_reasoning_effort="${quality === 'economy' ? 'low' : quality === 'balanced' ? 'medium' : 'high'}"`, '-'];
17
+ },
18
+
12
19
  resolveCommand({ env, which, platform, fileExists = existsSync }) {
13
20
  if (env.FULI_CODEX_BIN) return env.FULI_CODEX_BIN;
14
21
  const found = which('codex', platform);
@@ -22,14 +22,14 @@ function agentLink(application, agentId, name) {
22
22
  // The reply's attribution line: who led the task and who actually helped.
23
23
  export function taskAgentReceipt(application, { projectId, agentId, name = null,
24
24
  sourceApplication = null, workStatus = null, conversationId = null, persistence = null,
25
- collaborators = [] }) {
25
+ collaborators = [], assignmentStatus = null, assignmentReason = null }) {
26
26
  const owner = agentId ? { agent_id: agentId, name,
27
27
  profile_url: agentProfileUrl(application, agentId), source_application: sourceApplication } : null;
28
28
  const helpers = owner ? collaborators.filter(item => item.agentId !== agentId) : [];
29
29
  const markdown = owner
30
30
  ? `FULI Agent:负责人 ${agentLink(application, agentId, name)}`
31
31
  + (helpers.length ? ` · 协作 ${helpers.map(item => agentLink(application, item.agentId, item.name)).join('、')}` : '')
32
- : 'FULI Agent:未选定';
32
+ : unassignedReceipt(assignmentStatus, assignmentReason);
33
33
  return { required: true, markdown, owner,
34
34
  collaborators: helpers.map(item => ({ agent_id: item.agentId, name: item.name,
35
35
  profile_url: agentProfileUrl(application, item.agentId) })),
@@ -38,6 +38,24 @@ export function taskAgentReceipt(application, { projectId, agentId, name = null,
38
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
39
  }
40
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 尚未返回负责人)'}`;
57
+ }
58
+
41
59
  export function employeeCollaborationReceipt(application, context, { tool, permission, sourceApplication }) {
42
60
  return { agent_id: context.agentId, name: context.name, role: context.role,
43
61
  profile_url: agentProfileUrl(application, context.agentId),
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "tonborg",
4
+ "version": "1.0.0",
5
+ "name": "Tonborg",
6
+ "role": "判断官",
7
+ "description": "根据你的偏好与判断记录处理日常审核,为任务建议客户端和模型。",
8
+ "occupationEmoji": "⚖️",
9
+ "defaultProjectScope": "all",
10
+ "workbench": { "kind": "native", "view": "judgment" },
11
+ "capabilities": ["判断审核", "执行器选择", "判断反馈", "fuli.employee:tonborg"],
12
+ "workKinds": ["judgment", "knowledge-review", "executor-selection"],
13
+ "initialPreferences": [
14
+ "优先考虑用户明确偏好和当前任务证据;反馈只用于适用的范围。",
15
+ "记录判断依据与执行结果,权限不足或证据不足时交给用户。",
16
+ "保持项目负责人的汇报关系,不越过项目管理。"
17
+ ],
18
+ "permissions": ["judgment.read", "judgment.review"],
19
+ "runtime": null
20
+ }
@@ -29,7 +29,7 @@ export const employeeManifestSchema = z.object({
29
29
  }).strict().optional(),
30
30
  workbench: z.object({
31
31
  kind: z.literal('native'),
32
- view: z.literal('people')
32
+ view: z.enum(['people', 'judgment'])
33
33
  }).strict().optional(),
34
34
  runtime: z.object({
35
35
  apiVersion: z.literal(1),
@@ -45,7 +45,7 @@ export function parseEmployeeManifest(value) {
45
45
  }
46
46
 
47
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' });
48
+ export const FIXED_EMPLOYEE_AGENT_TYPES = Object.freeze({ jefa: 'coordinator', bole: 'hr', tonborg: 'durable' });
49
49
 
50
50
  export const employeeAgentId = (id) => `employee.${identifier.parse(id)}`;
51
51
  export const employeeCapability = (id) => `fuli.employee:${identifier.parse(id)}`;
@@ -186,7 +186,8 @@ export function createEmployeeRecruitment({ app, registry, managementStore }) {
186
186
  // All-project policy also covers projects without a materialized assignment.
187
187
  const agents = await app.listProjectAgents({ personalSpaceId: spaceId });
188
188
  const agent = agents.find((entry) => entry.agentId === employeeAgentId(manifest.id));
189
- const management = managementFor(manifest, agent, managementStore.read(spaceId, manifest.id));
189
+ const stored = managementStore.read(spaceId, manifest.id);
190
+ const management = managementFor(manifest, agent, stored);
190
191
  if (input.sourceApplication && Array.isArray(agent?.profile.allowedClients) && !agent.profile.allowedClients.includes(input.sourceApplication)) {
191
192
  throw new EmployeeError('This employee is not enabled for the current Agent client', 403, 'assignment_required');
192
193
  }
@@ -196,6 +197,24 @@ export function createEmployeeRecruitment({ app, registry, managementStore }) {
196
197
  (management.mode !== 'all' && !(agent.assignments ?? []).some((entry) => entry.personalProjectId === selectedProject.id && entry.status === 'active'))) {
197
198
  throw new EmployeeError('Recruit this employee into the selected project first', 403, 'assignment_required');
198
199
  }
200
+ if (manifest.id === 'tonborg' && input.ensureAssignment === true &&
201
+ !activeEmployeeAssignments(agent).some(entry => entry.personalProjectId === selectedProject.id)) {
202
+ // Provider memory is project-scoped. Preserve the already authorized all
203
+ // rule before its first concrete assignment, without launching a worker.
204
+ if (!stored) managementStore.write(spaceId, manifest.id, managementWithoutRevision(management), 0);
205
+ const history = (agent.assignments ?? []).filter(entry => entry.personalProjectId === selectedProject.id);
206
+ const generation = history.map(entry => `${entry.assignmentId}:${entry.revision ?? 0}`).sort();
207
+ const key = createHash('sha256').update(JSON.stringify([manifest.id, selectedProject.id, generation])).digest('hex');
208
+ await app.createProjectAgentAssignment({
209
+ personalSpaceId: spaceId, personalProjectId: selectedProject.id, agentId: agent.agentId,
210
+ idempotencyKey: `employee-assignment:${key}`, responsibility: agent.profile.responsibility,
211
+ workKinds: agent.profile.workKinds ?? manifest.workKinds, capabilities: agent.profile.capabilities,
212
+ reason: 'Initialize authorized Tonborg project memory',
213
+ });
214
+ // Assignment storage is asynchronous; a concurrent scope/client change
215
+ // must be observed before any judgment evidence is read.
216
+ return authorize({ ...input, ensureAssignment: false });
217
+ }
199
218
  return { personalSpaceId: spaceId, project: selectedProject, agentId: agent.agentId, management };
200
219
  }
201
220
 
@@ -30,11 +30,17 @@ export async function planAgentCollaboration(application, input) {
30
30
  }
31
31
  candidates.sort((a, b) => Number(a.requiresLoan) - Number(b.requiresLoan) || Number(b.workMatch) - Number(a.workMatch)
32
32
  || a.activeTasks - b.activeTasks || a.agentId.localeCompare(b.agentId));
33
+ const preferencePrompt = verificationPreference({ executors, rules, projectId: task.personalProjectId,
34
+ lockedExecutorIds: currentAgent.profile?.executorPolicy?.mode === 'locked' ? currentAgent.profile.executorPolicy.lockedExecutorIds : null });
35
+ const judgmentPolicy = await application.judgment?.policy({ personalSpaceId: spaceId, personalProjectId: task.personalProjectId });
36
+ if (judgmentPolicy && judgmentPolicy.mode !== 'manual' && preferencePrompt.choiceSource !== 'user_rule') {
37
+ Object.assign(preferencePrompt, { shouldAsk: false, recommendedExecutorId: null, choiceSource: 'tonborg',
38
+ guidance: 'The user delegated routine executor choices to Tonborg. Call assess_agent_action for the selected specialist before dispatch; ask only when its outcome requires user input. Recheck runtime availability and preserve the real execution receipt.' });
39
+ }
33
40
  return { teamLeadId: policy.team_lead_agent_id, currentAgentId: task.projectAgentId,
34
41
  candidates: candidates.slice(0, 8), pendingLoans: loans.loans?.filter(loan => loan.status === 'requested') ?? [],
35
42
  executionStarted: false,
36
- preferencePrompt: verificationPreference({ executors, rules, projectId: task.personalProjectId,
37
- lockedExecutorIds: currentAgent.profile?.executorPolicy?.mode === 'locked' ? currentAgent.profile.executorPolicy.lockedExecutorIds : null }),
43
+ preferencePrompt,
38
44
  guidance: 'Keep the project lead as owner. Choose a qualified specialist. For another project, the destination lead calls request_agent_loan; the source lead approves with decide_agent_loan in its own scoped task. Then coordinate_project_agent_task with the borrowed Agent. Share only the bounded task brief and authorized artifacts. Record actual workers. Verify each artifact; use record_agent_verification. Two failed attempts at one capability tier require escalation, not another identical retry. A selected model is not proof of execution or success.' };
39
45
  }
40
46
 
@@ -2,6 +2,9 @@ import { dirname, join } from 'node:path';
2
2
  import { createEmployeeService } from '../employees/service.js';
3
3
  import { createAgentRoundtable } from '../agent-roundtable/service.js';
4
4
  import { createRoundtableStore } from '../agent-roundtable/store.js';
5
+ import { createJudgmentService } from '../judgment/service.js';
6
+ import { JudgmentStore } from '../judgment/store.js';
7
+ import { createJudgmentEngine } from '../judgment/engine.js';
5
8
 
6
9
  import { GraphitiProviderClient } from './provider-client.js';
7
10
  import {
@@ -81,6 +84,7 @@ import {
81
84
  import { buildUserTasteSkill } from './user-taste-skill.js';
82
85
  import { taskEntryPreferences } from './task-entry-preferences.js';
83
86
  import { TASK_ENTRY_TOOLS, registerRepositoryProject } from './repository-project-registration.js';
87
+ import { resolveTaskEntryProject } from './task-entry-project-scope.js';
84
88
  import { getWritingTasteProfile as getWritingTasteProfileWorkflow } from './writing-taste-profile-workflow.js';
85
89
  import {
86
90
  groupSubscriptions,
@@ -119,7 +123,9 @@ export function openFederatedGraphApplication({
119
123
  fetchImpl
120
124
  });
121
125
  app.employees = createEmployeeService({ app, runtimeConfigPath });
122
- app.roundtable = createAgentRoundtable({ app,
126
+ app.judgment = createJudgmentService({ app, engine: createJudgmentEngine({ app }),
127
+ openStore: () => new JudgmentStore(join(dirname(runtimeConfigPath), 'judgment.sqlite')) });
128
+ app.roundtable = createAgentRoundtable({ app, env,
123
129
  openStore: () => createRoundtableStore(join(dirname(runtimeConfigPath), 'agent-roundtable.sqlite')) });
124
130
  }
125
131
  return app;
@@ -267,6 +273,9 @@ export class FederatedGraphApplication extends ProjectAgentControlPlaneApplicati
267
273
  // Every task gets a FULI Agent, so a task in an unregistered repository registers it.
268
274
  projectResolution = await registerRepositoryProject(this, projectResolution);
269
275
  }
276
+ projectResolution = await resolveTaskEntryProject(this, projectResolution, {
277
+ sessionId, sourceSessionId, sourceApplication, agentInvocation, agentToolName
278
+ });
270
279
  return taskEntryPreferences(this, projectResolution, {
271
280
  personalProjectId, projectAgentId, projectPath, taskPrompt, sourceApplication,
272
281
  sourceSessionId, sessionId, turnId, workKind, requiredCapabilities,
@@ -1160,7 +1169,7 @@ export class FederatedGraphApplication extends ProjectAgentControlPlaneApplicati
1160
1169
  };
1161
1170
  }
1162
1171
 
1163
- close() { this.roundtable?.close(); return this.employees?.close(); }
1172
+ close() { this.judgment?.close(); this.roundtable?.close(); return this.employees?.close(); }
1164
1173
 
1165
1174
  async #recordAgentViews(items, toolName) {
1166
1175
  const unique = new Map(items.map((item) => [
@@ -3,7 +3,8 @@ export function startKnowledgeReview(app, input) {
3
3
  return app.personal.startKnowledgeReview({
4
4
  personal_space_id: input.personalSpaceId,
5
5
  scope: input.scope,
6
- personal_project_id: input.personalProjectId ?? null
6
+ personal_project_id: input.personalProjectId ?? null,
7
+ ...(input.reviewer ? { reviewer: input.reviewer } : {})
7
8
  });
8
9
  }
9
10
 
@@ -12,7 +13,8 @@ export function listKnowledgeReviewCandidates(app, input) {
12
13
  return app.personal.listKnowledgeReviewCandidates({
13
14
  personal_space_id: input.personalSpaceId,
14
15
  review_id: input.reviewId,
15
- limit: input.limit
16
+ limit: input.limit,
17
+ ...(input.offset !== undefined ? { offset: input.offset } : {})
16
18
  });
17
19
  }
18
20
 
@@ -23,7 +25,9 @@ export function recordKnowledgeReviewProgress(app, input) {
23
25
  review_id: input.reviewId,
24
26
  candidate_key: input.candidateKey,
25
27
  outcome: input.outcome,
26
- note: input.note ?? null
28
+ note: input.note ?? null,
29
+ ...(input.aiReviewEvidenceToken ? { ai_review_evidence_token: input.aiReviewEvidenceToken } : {}),
30
+ ...(input.aiAssessment ? { ai_assessment: input.aiAssessment } : {})
27
31
  });
28
32
  }
29
33
 
@@ -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(
@@ -341,6 +343,8 @@ export function projectAgentTokenUsage(value) {
341
343
  export function providerProjectAgentTaskActivity(input) {
342
344
  const result = withoutUndefined({
343
345
  artifact_revision: input.artifactRevision ?? input.artifact_revision,
346
+ expected_verification_attempt_id: input.expectedVerificationAttemptId,
347
+ judgment_task_context_token: input.judgmentTaskContextToken,
344
348
  personal_space_id: input.personalSpaceId ?? input.personal_space_id,
345
349
  personal_project_id: input.personalProjectId ?? input.personal_project_id,
346
350
  task_id: input.taskId ?? input.task_id,
@@ -8,7 +8,11 @@ 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;
@@ -18,19 +22,33 @@ export async function resolveTaskEntryAgent(application, resolution, input) {
18
22
  personal_project_id: resolution.personalProjectId,
19
23
  source_application: input.sourceApplication ?? 'other'
20
24
  };
21
- let value = await application.personal.resolveProjectAgentContext({
25
+ const contextRequest = {
22
26
  ...scope,
23
27
  agent_id: mention?.agentId ?? input.projectAgentId ?? null,
24
28
  session_id: input.sessionId ?? null,
25
29
  turn_id: input.turnId ?? null,
26
30
  work_kind: workKind,
27
31
  required_capabilities: input.requiredCapabilities ?? []
32
+ };
33
+ const delegation = application.roundtable?.taskEntryDelegation?.({
34
+ sessionId: input.receivingSessionId ?? input.sessionId
28
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
+ }
29
47
  if (!value?.status) throw new Error('Role resolution is unavailable');
30
48
  let agent = value.agent ? projectAgentRecord(value.agent) : null;
31
49
  const implicit = !mention?.agentId && !input.projectAgentId;
32
50
  const peerOnly = agent && implicit && unrelatedPeer(agent, workKind, input.requiredCapabilities);
33
- if (implicit && (peerOnly || value.status === 'unassigned')) {
51
+ if (implicit && value.reason !== 'project_lead_required' && (peerOnly || value.status === 'unassigned')) {
34
52
  // Every task gets a FULI Agent: a project without a lead has HR hire one.
35
53
  const staffed = await application.personal.staffDefaultProjectLead(scope).catch(() => null);
36
54
  if (staffed?.agent) {
@@ -141,8 +159,8 @@ function boundedRoleText(value, limit) {
141
159
  function unrelatedPeer(agent, workKind, required = []) {
142
160
  const profile = agent.profile;
143
161
  const capabilities = (profile.capabilities ?? []).map(value => value.toLowerCase());
144
- const peer = ['employee.jefa', 'employee.bole', 'fuli-project-hr'].includes(agent.agentId)
145
- || profile.agentType === 'hr' || capabilities.some(value => ['fuli.employee:jefa', 'fuli.employee:bole'].includes(value));
162
+ const peer = ['employee.jefa', 'employee.bole', 'employee.tonborg', 'fuli-project-hr'].includes(agent.agentId)
163
+ || profile.agentType === 'hr' || capabilities.some(value => ['fuli.employee:jefa', 'fuli.employee:bole', 'fuli.employee:tonborg'].includes(value));
146
164
  return peer && !(profile.workKinds ?? []).some(kind => kind.toLowerCase() === workKind.toLowerCase())
147
165
  && !(required.length && required.every(value => capabilities.includes(value.toLowerCase())));
148
166
  }
@@ -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
  }
@@ -153,6 +153,15 @@ 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
+ }
156
165
  recentAgentSessions({ personalSpaceId, agentId, personalProjectId = null, limit = 5 }) {
157
166
  const query = new URLSearchParams({ personal_space_id: personalSpaceId, project_agent_id: agentId, limit: String(limit) });
158
167
  if (personalProjectId) query.set('personal_project_id', personalProjectId);