fuli-context 0.8.0 → 0.8.2

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 (132) hide show
  1. package/README.md +4 -0
  2. package/README.zh-CN.md +4 -1
  3. package/dist/web/assets/{AboutPage-BjMmpwJB.js → AboutPage-fBq5VFoQ.js} +1 -1
  4. package/dist/web/assets/AgentHand-CMZk5GjY.js +1 -0
  5. package/dist/web/assets/AgentName-Cydd0UI4.css +1 -0
  6. package/dist/web/assets/AgentName-DRZHAIB2.js +1 -0
  7. package/dist/web/assets/AgentProfilePage-BPQEfMhB.js +1 -0
  8. package/dist/web/assets/AgentsDirectoryPage-CKZfrcuD.js +1 -0
  9. package/dist/web/assets/{ConnectionsPage-BKGxFDos.js → ConnectionsPage-COb56qnk.js} +1 -1
  10. package/dist/web/assets/EmployeeRecruitDialog-BLTpjCcK.js +1 -0
  11. package/dist/web/assets/EmployeeWorkbenchPage-CQwZnTA0.css +1 -0
  12. package/dist/web/assets/EmployeeWorkbenchPage-HUd-gne2.js +3 -0
  13. package/dist/web/assets/{GrowthLoading-WXCj8b0r.js → GrowthLoading-Bu_kH-I8.js} +1 -1
  14. package/dist/web/assets/{KnowledgeEditDialog-Bkie08ZA.js → KnowledgeEditDialog-BTwXXRCD.js} +1 -1
  15. package/dist/web/assets/{KnowledgeInspector-Cez-Z_95.js → KnowledgeInspector-Ct5hl2--.js} +1 -1
  16. package/dist/web/assets/{KnowledgeOrganizerPage-BYmTPA_8.js → KnowledgeOrganizerPage-DMC8Hh43.js} +1 -1
  17. package/dist/web/assets/KnowledgePage-uXW1Eovz.js +1 -0
  18. package/dist/web/assets/{KnowledgeWorkspace-DKij2_B4.js → KnowledgeWorkspace-DmHIc0jb.js} +1 -1
  19. package/dist/web/assets/{NotFoundPage-HGV-q-bh.js → NotFoundPage-C0yOR9T9.js} +1 -1
  20. package/dist/web/assets/{OverviewPage-DE29_F_Y.js → OverviewPage-BreX_kb4.js} +1 -1
  21. package/dist/web/assets/{PersonalProfilePage-B95DFc7x.js → PersonalProfilePage-CX1a6_E3.js} +1 -1
  22. package/dist/web/assets/PersonalProjectsPage-Br1S8ETM.js +1 -0
  23. package/dist/web/assets/ProjectAgentsPage-CA04h1Uf.js +13 -0
  24. package/dist/web/assets/ProjectAgentsPage-CZWWV4Kw.css +1 -0
  25. package/dist/web/assets/{ProjectScopePicker-Cu5PdxF6.js → ProjectScopePicker-DVHfXX1f.js} +1 -1
  26. package/dist/web/assets/{PublicProjectsPage-BY4B3_t3.js → PublicProjectsPage-CtctKml1.js} +1 -1
  27. package/dist/web/assets/{ReviewPage-C9vCesNu.js → ReviewPage-DUqaK-do.js} +1 -1
  28. package/dist/web/assets/{SearchableSelect-214TvfYg.js → SearchableSelect-BZtyYmZN.js} +1 -1
  29. package/dist/web/assets/{SettingsPage-D9TnzcFc.js → SettingsPage-CB4ymLr8.js} +1 -1
  30. package/dist/web/assets/{SharedJefaReportPage-CdSGI_OE.js → SharedJefaReportPage-CbFtrDCY.js} +1 -1
  31. package/dist/web/assets/{WritingTastePage-CUE1dU8f.js → WritingTastePage-B7lSzs2z.js} +1 -1
  32. package/dist/web/assets/agent-profile-CdKOh5xw.css +1 -0
  33. package/dist/web/assets/agent-profile.css_vue_type_style_index_0_src_true_lang-C22UdfU0.js +1 -0
  34. package/dist/web/assets/catalog-DU4Kfd-N.js +1 -0
  35. package/dist/web/assets/{client-Bl4oiW3E.js → client-CEHkJJ-5.js} +1 -1
  36. package/dist/web/assets/{console-BAIVNC-N.js → console--CZxc3PW.js} +1 -1
  37. package/dist/web/assets/{conversation-launcher-settings-DpRI_Jp2.js → conversation-launcher-settings-Co1RlTFH.js} +1 -1
  38. package/dist/web/assets/{i18n-CVFADb_M.js → i18n-BtOOoyJd.js} +3 -3
  39. package/dist/web/assets/{identity-Dt0WZgqo.js → identity-CsowvtIm.js} +1 -1
  40. package/dist/web/assets/index-7X3LGByg.js +2 -0
  41. package/dist/web/assets/index-CWMqDq8g.css +1 -0
  42. package/dist/web/assets/{model-B8sOxih0.js → model-CGpllrds.js} +1 -1
  43. package/dist/web/assets/{personal-profile-graph-aAqex7ty.js → personal-profile-graph-BNTG4PSY.js} +1 -1
  44. package/dist/web/assets/profile-model-BQO4zNs9.js +1 -0
  45. package/dist/web/assets/{shared-report-BQD6l_cS.js → shared-report-BFUJqqQe.js} +1 -1
  46. package/dist/web/assets/task-evidence-BUu7yYIn.js +1 -0
  47. package/dist/web/assets/{useApi-CROJJdhE-CuJ16v2S.js → useApi-CROJJdhE-raNlHRDl.js} +1 -1
  48. package/dist/web/assets/{useModalDialog-Ddv2XJQi.js → useModalDialog-CroGk2Cs.js} +1 -1
  49. package/dist/web/assets/{vue-router-C8KuEZ6a.js → vue-router-JFYBLXB1.js} +1 -1
  50. package/dist/web/index.html +12 -10
  51. package/docs/agent-conversations-and-collaboration.md +115 -0
  52. package/docs/agent-interface-architecture.md +17 -0
  53. package/docs/employee-agents.md +14 -1
  54. package/docs/project-agent-memory.md +34 -1
  55. package/graph-provider/fuli_graph/agent_conversation_models.py +72 -0
  56. package/graph-provider/fuli_graph/agent_conversation_routes.py +47 -0
  57. package/graph-provider/fuli_graph/agent_loan_models.py +28 -0
  58. package/graph-provider/fuli_graph/agent_quality_policy.py +151 -0
  59. package/graph-provider/fuli_graph/agent_verification_models.py +22 -0
  60. package/graph-provider/fuli_graph/conversation_context.py +30 -0
  61. package/graph-provider/fuli_graph/knowledge_search.py +8 -16
  62. package/graph-provider/fuli_graph/project_agent_executor_models.py +3 -0
  63. package/graph-provider/fuli_graph/project_agent_memory_routes.py +2 -0
  64. package/graph-provider/fuli_graph/project_agent_models.py +10 -0
  65. package/graph-provider/fuli_graph/project_agent_routes.py +5 -1
  66. package/graph-provider/fuli_graph/project_agent_task_models.py +4 -0
  67. package/graph-provider/fuli_graph/runtime.py +14 -0
  68. package/graph-provider/fuli_graph/store.py +6 -0
  69. package/graph-provider/fuli_graph/store_agent_conversations.py +206 -0
  70. package/graph-provider/fuli_graph/store_agent_loans.py +146 -0
  71. package/graph-provider/fuli_graph/store_agent_verification.py +111 -0
  72. package/graph-provider/fuli_graph/store_project_agent_executors.py +2 -0
  73. package/graph-provider/fuli_graph/store_project_agent_task_activity.py +6 -0
  74. package/graph-provider/fuli_graph/store_project_agent_tasks.py +20 -8
  75. package/graph-provider/fuli_graph/store_task_contexts.py +9 -0
  76. package/graph-provider/fuli_graph/task_context_routes.py +7 -0
  77. package/graph-provider/pyproject.toml +1 -1
  78. package/npm-shrinkwrap.json +15 -15
  79. package/package.json +4 -3
  80. package/skills/capturing-session-knowledge/SKILL.md +20 -2
  81. package/skills/operating-fuli/SKILL.md +4 -0
  82. package/src/agent-tools/agent-collaboration-definitions.js +30 -0
  83. package/src/agent-tools/agent-conversation-definitions.js +21 -0
  84. package/src/agent-tools/employee-definitions.js +6 -5
  85. package/src/agent-tools/graph-definitions.js +19 -35
  86. package/src/agent-tools/graph-handlers.js +17 -1
  87. package/src/agent-tools/interface-catalog.js +2 -0
  88. package/src/agent-tools/project-agent-definitions.js +21 -0
  89. package/src/agent-tools/project-relation-definitions.js +36 -0
  90. package/src/agents/claude-code/conversation-transcript.js +20 -0
  91. package/src/agents/claude-code/lifecycle-hook.js +154 -0
  92. package/src/agents/claude-code/lifecycle-hooks.js +20 -0
  93. package/src/agents/codex/conversation-transcript.js +22 -0
  94. package/src/agents/codex/lifecycle-hook.js +47 -8
  95. package/src/agents/codex/lifecycle-hooks.js +6 -6
  96. package/src/agents/cursor/lifecycle-hook.js +5 -4
  97. package/src/app/agent-request-context.js +6 -1
  98. package/src/app/test-tools.js +26 -0
  99. package/src/conversations/sync-transcript.js +144 -0
  100. package/src/conversations/task-context-view.js +28 -0
  101. package/src/conversations/transcript-guard.js +54 -0
  102. package/src/conversations/transcript-reader.js +113 -0
  103. package/src/employees/service.js +8 -1
  104. package/src/graphiti/agent-collaboration.js +62 -0
  105. package/src/graphiti/agent-conversations.js +75 -0
  106. package/src/graphiti/agent-knowledge-workflows.js +8 -2
  107. package/src/graphiti/project-agent-control-plane.js +22 -2
  108. package/src/graphiti/project-agent-mapping.js +19 -1
  109. package/src/graphiti/project-agent-task-entry.js +11 -1
  110. package/src/graphiti/project-agent-workflows.js +11 -1
  111. package/src/graphiti/project-path-context.js +17 -10
  112. package/src/graphiti/provider-client.js +17 -0
  113. package/src/graphiti/verification-preference.js +24 -0
  114. package/src/http/graph-api-router.js +17 -0
  115. package/src/mcp/create-mcp-server.js +19 -5
  116. package/src/mcp/employee-tool-catalog.js +22 -0
  117. package/src/mcp/session-id.js +4 -0
  118. package/src/mcp/tool-annotations.js +8 -1
  119. package/src/mcp/tool-result.js +3 -1
  120. package/src/setup/agent-installation-status.js +1 -0
  121. package/src/setup/claude-code-config.js +13 -19
  122. package/dist/web/assets/AgentHand-BfLYPGjg.js +0 -1
  123. package/dist/web/assets/EmployeeRecruitDialog-D-W043v4.js +0 -1
  124. package/dist/web/assets/EmployeeWorkbenchPage-Cz7y-IFD.css +0 -1
  125. package/dist/web/assets/EmployeeWorkbenchPage-HHQPxdqI.js +0 -3
  126. package/dist/web/assets/KnowledgePage-Ck7tIBDn.js +0 -1
  127. package/dist/web/assets/PersonalProjectsPage-Bt7tFF5Z.js +0 -1
  128. package/dist/web/assets/ProjectAgentsPage-BioDR9OW.css +0 -1
  129. package/dist/web/assets/ProjectAgentsPage-CiiPNe_G.js +0 -12
  130. package/dist/web/assets/index-BUMq5zNh.js +0 -2
  131. package/dist/web/assets/index-C0ci8dnU.css +0 -1
  132. package/src/mcp/task-context-registry.js +0 -165
@@ -3,6 +3,7 @@ import { booleanSchema, objectSchema, stringSchema } from './schema.js';
3
3
  const id = { ...stringSchema(), minLength: 1, maxLength: 256 };
4
4
  const projectPath = { ...stringSchema(), minLength: 1, maxLength: 4096 };
5
5
  const target = { projectPath, templateId: id };
6
+ const workbenchTarget = { ...target, personalProjectId: id };
6
7
 
7
8
  export const EMPLOYEE_TOOL_DEFINITIONS = [
8
9
  {
@@ -29,15 +30,15 @@ export const EMPLOYEE_TOOL_DEFINITIONS = [
29
30
  },
30
31
  {
31
32
  name: 'list_employee_tools', title: 'READ · Employee workbench tools',
32
- description: 'Discover the exact input schemas and permissions for a recruited employee in the current project. Read these schemas before calling a tool; a workbench has to be installed first.',
33
- inputSchema: objectSchema(target, ['projectPath', 'templateId'])
33
+ description: 'Discover complete input schemas and permitted tools for a recruited employee. Supply projectPath, or a previously resolved personalProjectId without projectPath when directory resolution is unavailable. If both are supplied they must resolve to the same project. Existing project and employee authorization always apply. Use includeSchemas=false for a compact name list, then toolName to read one exact schema. Oversized contracts fail explicitly, never return partial schemas. Read the schema before calling; a workbench has to be installed first.',
34
+ inputSchema: objectSchema({ ...workbenchTarget, toolName: id, includeSchemas: booleanSchema() }, ['templateId'])
34
35
  },
35
36
  {
36
37
  name: 'call_employee_tool', title: 'WRITE · Call an employee workbench tool',
37
- description: 'Call a discovered employee tool inside the exact assigned project. May read or change data according to the tool schema. Preserve optimistic concurrency and idempotency keys. Does not grant cross-project access, execute an external model, or confirm task completion for the human.',
38
+ description: 'Call a discovered employee tool inside the exact assigned project. Supply projectPath or a previously resolved personalProjectId; omit projectPath if directory resolution is unavailable. If both are supplied they must resolve to the same project. May read or change data according to the tool schema. Preserve optimistic concurrency and idempotency keys. Does not grant cross-project access, execute an external model, or confirm task completion for the human.',
38
39
  inputSchema: objectSchema({
39
- ...target, tool: id,
40
+ ...workbenchTarget, tool: id,
40
41
  arguments: { type: 'object', additionalProperties: true }
41
- }, ['projectPath', 'templateId', 'tool', 'arguments'])
42
+ }, ['templateId', 'tool', 'arguments'])
42
43
  }
43
44
  ];
@@ -1,3 +1,6 @@
1
+ import { projectRelationDefinitions } from './project-relation-definitions.js';
2
+ import { AGENT_COLLABORATION_DEFINITIONS } from './agent-collaboration-definitions.js';
3
+ import { AGENT_CONVERSATION_DEFINITIONS } from './agent-conversation-definitions.js';
1
4
  import {
2
5
  arraySchema,
3
6
  booleanSchema,
@@ -670,6 +673,10 @@ export const GRAPH_TOOL_DEFINITIONS = [
670
673
  description: 'Resolve the exact local project from projectPath and return its lightweight Agent directory. Optionally filter active/inactive/archived records or find Agents whose capabilities or responsibility match a capability query. This never loads another Agent\'s private memory.',
671
674
  inputSchema: objectSchema({
672
675
  projectPath: boundedString(4096),
676
+ personalProjectId: {
677
+ ...nullableStringSchema(), minLength: 1, maxLength: 128,
678
+ description: 'Explicit existing project ID when the directory has no exact match. A conflicting exact path match is rejected.'
679
+ },
673
680
  status: nullableProjectAgentStatus,
674
681
  capability: nullableStringSchema()
675
682
  }, ['projectPath'])
@@ -680,6 +687,10 @@ export const GRAPH_TOOL_DEFINITIONS = [
680
687
  description: 'Resolve one active project Agent and assemble only the context needed for the task: personal-global rules, exact and authorized inherited project knowledge, this Agent\'s preferences and memory, and focused query results. It excludes every other project Agent. Fuli remains a control layer; the calling host Agent performs reasoning and execution.',
681
688
  inputSchema: objectSchema({
682
689
  projectPath: boundedString(4096),
690
+ personalProjectId: {
691
+ ...nullableStringSchema(), minLength: 1, maxLength: 128,
692
+ description: 'Explicit existing project ID when the directory has no exact match. A conflicting exact path match is rejected.'
693
+ },
683
694
  agentId: boundedString(128),
684
695
  queries: arraySchema(shortText, { minItems: 1, maxItems: 10 }),
685
696
  limitPerQuery: integerSchema({ minimum: 1, maximum: 50 }),
@@ -697,6 +708,8 @@ export const GRAPH_TOOL_DEFINITIONS = [
697
708
  }, ['personalSpaceId', 'agentId'])
698
709
  },
699
710
  ...PROJECT_AGENT_MEMORY_DEFINITIONS,
711
+ ...AGENT_CONVERSATION_DEFINITIONS,
712
+ ...AGENT_COLLABORATION_DEFINITIONS,
700
713
  {
701
714
  name: 'delete_project_agent',
702
715
  title: 'WRITE · Archive one Agent identity',
@@ -710,7 +723,7 @@ export const GRAPH_TOOL_DEFINITIONS = [
710
723
  {
711
724
  name: 'cleanup_test_project_agents',
712
725
  title: 'WRITE · Archive test Agent roles',
713
- description: 'Archive only test-marked Project Agents for one explicit test source through the Provider. Durable production identities are not touched; the result reports the number archived for cleanup verification.',
726
+ description: 'TEST-ONLY. Archive only test-marked Project Agents for one explicit test source through the Provider. Disabled on default MCP/HTTP surfaces unless FULI_ENABLE_TEST_TOOLS=1. Durable production identities are not touched; the result reports the number archived for cleanup verification.',
714
727
  inputSchema: objectSchema({
715
728
  personalSpaceId: id,
716
729
  testSource: boundedString(256)
@@ -748,7 +761,7 @@ export const GRAPH_TOOL_DEFINITIONS = [
748
761
  {
749
762
  name: 'coordinate_project_agent_task',
750
763
  title: 'WRITE · Prepare a host-executed Agent team',
751
- description: 'Resolve the exact local project, let the Provider coordinator route one durable task, and assemble an isolated context bundle for every selected lead/collaborator. Pass the current taskContextToken so a newly recruited lead can claim an initially unassigned task, including its blocked work. The result is a truthful host execution plan: Fuli persists identity, staffing, and memory scope, while the calling host must start real workers, report their concrete worker/executor evidence, and finish their lifecycle. This tool never claims that a selected Agent is online or that a worker was started.',
764
+ description: 'Resolve the exact local project, let the Provider coordinator route one durable task, and assemble an isolated context bundle for every selected lead/collaborator. Pass the current taskContextToken so a newly recruited lead can claim an initially unassigned task, including its blocked work. When recruiting, HR should set recruitmentProfile.character from the role responsibilities and preserve user expectations; configured traits must never be presented as proven growth. The result is a truthful host execution plan: Fuli persists identity, staffing, and memory scope, while the calling host must start real workers, report their concrete worker/executor evidence, and finish their lifecycle. This tool never claims that a selected Agent is online or that a worker was started.',
752
765
  inputSchema: projectAgentTaskCoordinateInput
753
766
  },
754
767
  {
@@ -776,7 +789,7 @@ export const GRAPH_TOOL_DEFINITIONS = [
776
789
  {
777
790
  name: 'submit_project_agent_task',
778
791
  title: 'WRITE · Submit a routed Agent task',
779
- description: 'Submit one durable task to the FULI control plane. Existing matching durable Agents are preferred; no anonymous temporary Agent is created. The Agent locked executor policy outranks task, assignment, project, and space rules; explicit user priority is never lowered for token savings. The space coordinator, complexity, model strategy, routing decision, boundaries, actual executor/model/rule/fallback audit fields, and any HR disclosure are persisted by the Provider.',
792
+ description: 'Submit one durable task to the FULI control plane. Every new task requires a passed quality gate for its exact artifact revision before completion; use record_agent_verification with actual run evidence. Existing matching durable Agents are preferred; no anonymous temporary Agent is created. The Agent locked executor policy outranks task, assignment, project, and space rules; explicit user priority is never lowered for token savings. The space coordinator, complexity, model strategy, routing decision, boundaries, actual executor/model/rule/fallback audit fields, and any HR disclosure are persisted by the Provider. HR should fill recruitmentProfile.character from role responsibilities, preserve user expectations, and distinguish configured traits from demonstrated growth; never invent past achievements.',
780
793
  inputSchema: projectAgentTaskSubmitInput
781
794
  },
782
795
  {
@@ -793,7 +806,7 @@ export const GRAPH_TOOL_DEFINITIONS = [
793
806
  {
794
807
  name: 'record_project_agent_task_activity',
795
808
  title: 'WRITE · Record task activity',
796
- description: 'Record one explicit task event, including real terminal activity, actual executor/model/client attribution, and an optional source-labelled cumulative token snapshot for the concrete worker. Never estimate missing usage. Terminal activity is the only source for per-Agent activity summaries.',
809
+ description: 'Record real task/worker activity and optional source-labelled cumulative token usage; never estimate. workerStatus requires workerId and agentId. Report actualExecutorId, actualModelProvider and actualModel together only with matching authorized preflight evidence. Otherwise omit all three, report observed workerRuntime and explain the mismatch in summary. A terminal activity with valid actual execution automatically records its routing outcome.',
797
810
  inputSchema: projectAgentTaskActivityInput
798
811
  },
799
812
  {
@@ -1003,7 +1016,7 @@ export const GRAPH_TOOL_DEFINITIONS = [
1003
1016
  {
1004
1017
  name: 'record_project_agent_task_outcome',
1005
1018
  title: 'WRITE · Record explicit routing outcome evidence',
1006
- description: 'Record only explicit acceptance/satisfaction evidence such as rework_requested, repeated_negative_feedback, explicit_praise, test/acceptance pass or fail, or an explicit rating. Natural-language text is never used to infer satisfaction.',
1019
+ description: 'Record explicit user/test outcome evidence against a matching recorded execution. Use the task workKind and effective modelStrategy plus real referenceIds; a CLI session ID alone is not a recorded run. Terminal routing outcomes are already derived from valid task activity; do not submit them again. Never infer satisfaction or fabricate execution evidence to clear a validation error.',
1007
1020
  inputSchema: projectAgentTaskOutcomeInput
1008
1021
  },
1009
1022
  {
@@ -1194,36 +1207,7 @@ export const GRAPH_TOOL_DEFINITIONS = [
1194
1207
  providerUrl: shortText
1195
1208
  }, ['projectId', 'providerUrl'])
1196
1209
  },
1197
- {
1198
- name: 'create_project_relation',
1199
- description: 'Create a canonical relationship between two public projects. PART_OF waits for target/parent Maintainer confirmation; other relation types become active immediately. This never subscribes either project.',
1200
- inputSchema: objectSchema({
1201
- sourceProjectId: id,
1202
- targetProjectId: id,
1203
- providerUrl: shortText,
1204
- relationType: projectRelationType,
1205
- note: nullableStringSchema()
1206
- }, ['sourceProjectId', 'targetProjectId', 'providerUrl', 'relationType'])
1207
- },
1208
- {
1209
- name: 'list_project_relations',
1210
- description: 'List canonical incoming and outgoing project relationships. Related projects are suggestions only and never expand the active subscription set.',
1211
- inputSchema: objectSchema({
1212
- projectId: id,
1213
- providerUrl: shortText
1214
- }, ['projectId', 'providerUrl'])
1215
- },
1216
- {
1217
- name: 'review_project_relation',
1218
- description: 'Confirm or reject a pending PART_OF relationship as a Maintainer of the target parent project.',
1219
- inputSchema: objectSchema({
1220
- targetProjectId: id,
1221
- relationId: id,
1222
- providerUrl: shortText,
1223
- decision: enumSchema(['confirm', 'reject']),
1224
- note: nullableStringSchema()
1225
- }, ['targetProjectId', 'relationId', 'providerUrl', 'decision'])
1226
- },
1210
+ ...projectRelationDefinitions({ id, shortText, projectRelationType }),
1227
1211
  {
1228
1212
  name: 'list_personal_review_queue',
1229
1213
  description: 'List full structured knowledge drafts waiting in the local personal pre-review queue before any public submission.',
@@ -1,7 +1,20 @@
1
1
  import { ApplicationError } from '../app/application-error.js';
2
+ import { assertTestToolsEnabled } from '../app/test-tools.js';
3
+ import { activeTestToolsEnabled } from '../app/agent-request-context.js';
2
4
  import { agentInterfaceCatalog } from './interface-catalog.js';
3
5
 
4
6
  const HANDLERS = Object.freeze({
7
+ get_agent_quality_gate: (app, input) => app.agentVerification('query', input),
8
+ record_agent_verification: (app, input) => app.agentVerification('record', input),
9
+ plan_agent_collaboration: (app, input) => app.planAgentCollaboration(input),
10
+ request_agent_loan: (app, input) => app.agentLoan('request', input),
11
+ decide_agent_loan: (app, input) => app.agentLoan('decide', input),
12
+ list_agent_loans: (app, input) => app.agentLoan('query', input),
13
+ list_agent_conversations: (app, input) => app.queryAgentConversations({ ...input, mode: 'list' }),
14
+ read_agent_conversation: (app, input) => app.queryAgentConversations({ ...input, mode: 'events' }),
15
+ resume_agent_conversation: (app, input) => app.resumeAgentConversation(input),
16
+ get_agent_conversation_policy: (app, input) => app.queryAgentConversations({ ...input, mode: 'policy' }),
17
+ update_agent_conversation_policy: (app, input) => app.updateAgentConversationPolicy(input),
5
18
  list_project_agent_tasks: (app, input) => app.listProjectAgentTasks(input),
6
19
  request_agent_attention: (app, input) => app.requestAgentAttention(input),
7
20
  list_agent_attention: (app, input) => app.listAgentAttention(input),
@@ -107,7 +120,10 @@ const HANDLERS = Object.freeze({
107
120
  list_project_agents: (app, input) => app.listCurrentProjectAgents(input),
108
121
  get_project_agent: (app, input) => app.getProjectAgent(input),
109
122
  delete_project_agent: (app, input) => app.deleteProjectAgent(input),
110
- cleanup_test_project_agents: (app, input) => app.cleanupProjectAgentTestRoles(input),
123
+ cleanup_test_project_agents: (app, input) => {
124
+ assertTestToolsEnabled({ FULI_ENABLE_TEST_TOOLS: activeTestToolsEnabled() ? '1' : '0' });
125
+ return app.cleanupProjectAgentTestRoles(input);
126
+ },
111
127
  create_project_agent_assignment: (app, input) =>
112
128
  app.createProjectAgentAssignment(input),
113
129
  list_project_agent_assignments: (app, input) =>
@@ -1,4 +1,6 @@
1
1
  const UI_MUTATION_PARITY = Object.freeze([
2
+ agent('POST /api/agent-conversations/query', 'list_agent_conversations', 'read_agent_conversation', 'get_agent_conversation_policy'),
3
+ agent('PUT /api/agent-conversations/policy', 'update_agent_conversation_policy'),
2
4
  agent('POST /api/agent-attention', 'request_agent_attention'),
3
5
  agent('POST /api/agent-attention/cancel', 'cancel_agent_attention'),
4
6
  local('POST /api/agent-attention/respond', 'Only a local person may respond to an Agent request. A response records a decision, but never executes permissions or task acceptance.'),
@@ -4,6 +4,7 @@ import {
4
4
  enumSchema,
5
5
  integerSchema,
6
6
  nullableStringSchema,
7
+ numberSchema,
7
8
  objectSchema,
8
9
  stringSchema
9
10
  } from './schema.js';
@@ -54,6 +55,18 @@ export const projectAgentProfile = objectSchema({
54
55
  name: boundedString(160),
55
56
  displayName: boundedString(160),
56
57
  responsibility: boundedString(4096),
58
+ character: {
59
+ ...objectSchema({
60
+ judgment: { type: 'string', maxLength: 2048, default: '' },
61
+ taste: { type: 'string', maxLength: 2048, default: '' },
62
+ personality: { type: 'string', maxLength: 2048, default: '' }
63
+ }),
64
+ description: 'HR-configured judgment, taste and personality based on role responsibilities. These are intended working traits, not evidence of observed growth or performance.'
65
+ },
66
+ expectations: {
67
+ type: 'string', maxLength: 4096, default: '',
68
+ description: 'User expectations for this Agent. Preserve the user’s wording and leave empty when none were provided.'
69
+ },
57
70
  occupationEmoji: { ...nullableStringSchema(), minLength: 1, maxLength: 32 },
58
71
  agentType: enumSchema(['coordinator', 'durable', 'hr', 'temporary']),
59
72
  workKinds: arraySchema(boundedString(512), { maxItems: 32 }),
@@ -156,6 +169,10 @@ export const projectAgentTaskSubmitInput = objectSchema({
156
169
  export const projectAgentTaskCoordinateInput = objectSchema({
157
170
  taskContextToken: id,
158
171
  projectPath: boundedString(4096),
172
+ personalProjectId: {
173
+ ...nullableStringSchema(), minLength: 1, maxLength: 128,
174
+ description: 'Explicit existing project ID when the directory has no exact match. A conflicting exact path match is rejected.'
175
+ },
159
176
  idempotencyKey,
160
177
  title: boundedString(160),
161
178
  objective: boundedString(4096),
@@ -181,6 +198,7 @@ export const projectAgentTaskCoordinateInput = objectSchema({
181
198
  'routingReason', 'contextQueries'
182
199
  ]);
183
200
  export const projectAgentTaskActivityInput = objectSchema({
201
+ artifactRevision: { ...stringSchema(), minLength: 1, maxLength: 160 },
184
202
  personalSpaceId: id,
185
203
  personalProjectId: id,
186
204
  taskId: id,
@@ -235,6 +253,8 @@ const executorActualModelStrategySource = enumSchema([
235
253
  'task', 'assignment', 'agent', 'routing_rule', 'coordinator'
236
254
  ]);
237
255
  export const executorModel = objectSchema({
256
+ capabilityTier: integerSchema({ minimum: 1, maximum: 3 }),
257
+ costRank: numberSchema({ minimum: 0 }),
238
258
  provider: boundedString(128),
239
259
  model: boundedString(256),
240
260
  capabilities: arraySchema(boundedString(512), { maxItems: 32 }),
@@ -276,6 +296,7 @@ export const executorRoutingRuleInput = objectSchema({
276
296
  idempotencyKey
277
297
  }, ['scope', 'workKind', 'reason', 'idempotencyKey']);
278
298
  export const executorActualReportInput = objectSchema({
299
+ artifactRevision: { ...stringSchema(), minLength: 1, maxLength: 160 },
279
300
  personalSpaceId: id,
280
301
  personalProjectId: id,
281
302
  taskId: id,
@@ -0,0 +1,36 @@
1
+ import { enumSchema, nullableStringSchema, objectSchema } from './schema.js';
2
+
3
+ export function projectRelationDefinitions({ id, shortText, projectRelationType }) {
4
+ return [
5
+ {
6
+ name: 'create_project_relation',
7
+ description: 'Create a canonical relationship between two public projects. PART_OF waits for target/parent Maintainer confirmation; other relation types become active immediately. This never subscribes either project.',
8
+ inputSchema: objectSchema({
9
+ sourceProjectId: id,
10
+ targetProjectId: id,
11
+ providerUrl: shortText,
12
+ relationType: projectRelationType,
13
+ note: nullableStringSchema()
14
+ }, ['sourceProjectId', 'targetProjectId', 'providerUrl', 'relationType'])
15
+ },
16
+ {
17
+ name: 'list_project_relations',
18
+ description: 'List canonical incoming and outgoing project relationships. Related projects are suggestions only and never expand the active subscription set.',
19
+ inputSchema: objectSchema({
20
+ projectId: id,
21
+ providerUrl: shortText
22
+ }, ['projectId', 'providerUrl'])
23
+ },
24
+ {
25
+ name: 'review_project_relation',
26
+ description: 'Confirm or reject a pending PART_OF relationship as a Maintainer of the target parent project.',
27
+ inputSchema: objectSchema({
28
+ targetProjectId: id,
29
+ relationId: id,
30
+ providerUrl: shortText,
31
+ decision: enumSchema(['confirm', 'reject']),
32
+ note: nullableStringSchema()
33
+ }, ['targetProjectId', 'relationId', 'providerUrl', 'decision'])
34
+ },
35
+ ];
36
+ }
@@ -0,0 +1,20 @@
1
+ // Claude JSONL v1; thinking/redacted_thinking are intentionally never persisted.
2
+ export function normalizeClaudeRecord(record, sessionId) {
3
+ if (record?.sessionId !== sessionId || !['user', 'assistant'].includes(record.type)) return [];
4
+ const content = record.message?.content;
5
+ if (typeof content === 'string') return content ? [{ role: record.type, kind: 'message', content }] : [];
6
+ if (!Array.isArray(content)) return [];
7
+ return content.flatMap(block => {
8
+ if (block.type === 'text' && block.text) return [{ role: record.type, kind: 'message', content: block.text }];
9
+ if (block.type === 'tool_use') return [{ role: 'assistant', kind: 'tool_call', content: JSON.stringify({
10
+ call_id: block.id, name: block.name, input: block.input
11
+ }) }];
12
+ if (block.type === 'tool_result') return [{ role: 'tool', kind: 'tool_result', content: JSON.stringify({
13
+ call_id: block.tool_use_id, output: block.content, is_error: block.is_error ?? false
14
+ }) }];
15
+ return [];
16
+ });
17
+ }
18
+ export function verifyClaudeTranscript(firstRecord, sessionId) {
19
+ return firstRecord?.sessionId === sessionId;
20
+ }
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ import { withTranscriptGuard } from '../../conversations/transcript-guard.js';
3
+ import { compactTaskContext } from '../../conversations/task-context-view.js';
4
+ import { syncConversationTranscript, withTranscriptNotice, claimTranscriptBoundary, recordTranscriptTaskEntry } from '../../conversations/sync-transcript.js';
5
+ import { normalizeClaudeRecord, verifyClaudeTranscript } from './conversation-transcript.js';
6
+ import { resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ import { callAgentTool } from '../../agent-tools.js';
10
+ import { createRuntimeLeaseClient } from '../../adaptive-runtime/lease-client.js';
11
+ import { openFederatedGraphApplication } from '../../graphiti/federated-application.js';
12
+ import { resolveGraphRuntimeOptions } from '../../graphiti/runtime-config.js';
13
+ import { hookAdditionalContextToolResult } from '../../mcp/tool-result.js';
14
+ import { boundedHookMessage } from '../lifecycle-message.js';
15
+
16
+ export async function claudeLifecycleOutput(event, input, invoke) {
17
+ if (!['UserPromptSubmit', 'Stop'].includes(event)) throw new TypeError('Unsupported Claude lifecycle event');
18
+ const sessionId = input?.session_id;
19
+ if (typeof sessionId !== 'string' || !sessionId.trim() || sessionId.length > 256) {
20
+ throw new TypeError('Claude lifecycle requires a host session identifier');
21
+ }
22
+ const source = { sourceApplication: 'claude_code', sourceSessionId: sessionId };
23
+ if (event === 'UserPromptSubmit') {
24
+ if (typeof input.cwd !== 'string' || !input.cwd.trim() || typeof input.prompt !== 'string') {
25
+ throw new TypeError('Claude task entry requires cwd and prompt');
26
+ }
27
+ const context = await invoke('begin_task_context', {
28
+ sessionId, projectPath: input.cwd, taskPrompt: input.prompt.slice(0, 8192), ...source
29
+ });
30
+ return JSON.parse(hookAdditionalContextToolResult(compactTaskContext(context), {
31
+ hookEventName: event,
32
+ label: 'Fuli task context. Apply effective_preferences and use taskContextToken for the final checkpoint.',
33
+ limitBytes: 128 * 1024,
34
+ itemLimit: 1000
35
+ }).content[0].text);
36
+ }
37
+ const check = await invoke('verify_task_checkpoint', { sessionId, ...source });
38
+ const reason = boundedHookMessage(check?.reason);
39
+ if (check?.decision !== 'block' || !reason.trim()) return {};
40
+ if (input.stop_hook_active === true) {
41
+ if (typeof check.task_context_token === 'string' && check.task_context_token.trim()) {
42
+ await invoke('checkpoint_task_knowledge', {
43
+ taskContextToken: check.task_context_token,
44
+ disposition: 'retain_nothing',
45
+ reason: 'Claude Code Stop hook fallback after one checkpoint continuation.',
46
+ workLog: { status: 'incomplete', summary: 'The host stopped before the employee submitted a work summary. Review the prior task before resuming.' },
47
+ ...source
48
+ });
49
+ }
50
+ return {};
51
+ }
52
+ return { decision: 'block', reason };
53
+ }
54
+
55
+ export function claudeLifecycleUnavailable(event) {
56
+ const warning = 'Fuli context is unavailable; memory recovery and task checkpoint are not verified. '
57
+ + 'Continue with the user request and use Fuli MCP when it becomes available.';
58
+ return event === 'UserPromptSubmit'
59
+ ? { systemMessage: warning, hookSpecificOutput: { hookEventName: event, additionalContext: warning } }
60
+ : { systemMessage: warning };
61
+ }
62
+
63
+ export async function runClaudeLifecycleHook(args, dependencies = {}) {
64
+ const event = args[args.indexOf('--event') + 1];
65
+ const input = await (dependencies.readInput ?? readHookInput)();
66
+ if (dependencies.invoke) return claudeLifecycleOutput(event, input, dependencies.invoke);
67
+ const { runtimeConfigPath } = (dependencies.resolveRuntimeOptions ?? resolveGraphRuntimeOptions)(
68
+ args, dependencies.env ?? process.env
69
+ );
70
+ let app;
71
+ let leases;
72
+ try {
73
+ app = (dependencies.openApplication ?? openFederatedGraphApplication)({ runtimeConfigPath });
74
+ leases = (dependencies.createLeases ?? createRuntimeLeaseClient)({ runtimeConfigPath });
75
+ const request = { signal: dependencies.signal };
76
+ return await leases.withGraphLease('claude-code-lifecycle', async () => {
77
+ try {
78
+ if (app.getCapturePolicy?.().enabled && input.transcript_path) dependencies.onTranscriptGuard?.();
79
+ return await withTranscriptGuard(runtimeConfigPath, input, 'claude_code', app.getCapturePolicy?.().enabled, async guard => {
80
+ let sync = await syncConversationTranscript(app, input, 'claude_code', {
81
+ normalize: normalizeClaudeRecord, verify: verifyClaudeTranscript
82
+ }, guard);
83
+ if (event === 'UserPromptSubmit' && sync.entryBlocked) return { decision: 'block', reason: sync.reason };
84
+ const entryGuard = sync.status === 'capture_disabled' ? null : guard;
85
+ if (event === 'UserPromptSubmit' && entryGuard?.value) await entryGuard.write({ ...entryGuard.value, phase: 'beginning' });
86
+ const output = await claudeLifecycleOutput(event, input, async (name, parameters) => {
87
+ const context = await (dependencies.callTool ?? callAgentTool)(app, name, parameters, request);
88
+ if (name === 'begin_task_context') await recordTranscriptTaskEntry(entryGuard, context);
89
+ return context;
90
+ });
91
+ if (event === 'UserPromptSubmit') sync = await claimTranscriptBoundary(app, input, 'claude_code', sync, {
92
+ normalize: normalizeClaudeRecord, verify: verifyClaudeTranscript
93
+ }, entryGuard);
94
+ if (event === 'UserPromptSubmit' && sync.entryBlocked) return { decision: 'block', reason: sync.reason };
95
+ return withTranscriptNotice(output, sync);
96
+ });
97
+ } catch (error) {
98
+ if (event !== 'UserPromptSubmit' || !app.getCapturePolicy?.().enabled || !input.transcript_path) throw error;
99
+ return { decision: 'block', reason: error.code === 'EADDRINUSE'
100
+ ? 'The session handoff lock is occupied. Retry after the current hook finishes; if it persists, start a new host session and resume the intended Agent.'
101
+ : 'Fuli transcript handoff could not be completed safely. Retry task entry; no previous Agent context was supplied.' };
102
+ }
103
+ }, request);
104
+ } finally {
105
+ await Promise.allSettled([close(leases), close(app)]);
106
+ }
107
+ }
108
+
109
+ async function close(resource) { await resource?.close(); }
110
+
111
+ async function readHookInput() {
112
+ const chunks = [];
113
+ let size = 0;
114
+ for await (const chunk of process.stdin) {
115
+ size += chunk.length;
116
+ if (size > 256 * 1024) throw new TypeError('Hook input exceeds limit');
117
+ chunks.push(chunk);
118
+ }
119
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
120
+ }
121
+
122
+ async function main() {
123
+ const args = process.argv.slice(2);
124
+ const event = args[args.indexOf('--event') + 1];
125
+ const configured = args.includes('--timeout-ms') ? Number(args[args.indexOf('--timeout-ms') + 1]) : 28000;
126
+ const timeoutMs = Number.isSafeInteger(configured) && configured > 0 && configured <= 2_147_483_647
127
+ ? configured : 28000;
128
+ const controller = new AbortController();
129
+ let emitted = false;
130
+ let guarded = false;
131
+ const emit = (output, callback) => {
132
+ if (emitted) return;
133
+ emitted = true;
134
+ process.stdout.write(`${JSON.stringify(output)}\n`, callback);
135
+ };
136
+ // Finish before the host timeout, even if a dependency ignores cancellation.
137
+ // No prompt, transcript, path or raw exception is written to diagnostics.
138
+ const deadline = setTimeout(() => {
139
+ controller.abort();
140
+ emit(guarded && event === 'UserPromptSubmit' ? { decision: 'block', reason: 'Fuli transcript handoff timed out. Retry task entry; the previous Agent context was not supplied.' } : claudeLifecycleUnavailable(event), () => process.exit(0));
141
+ }, timeoutMs);
142
+ const abort = setTimeout(() => controller.abort(), Math.max(1, timeoutMs - 1500));
143
+ try {
144
+ const output = await runClaudeLifecycleHook(args, { signal: controller.signal, onTranscriptGuard: () => { guarded = true; } });
145
+ emit(output);
146
+ } catch {
147
+ emit(claudeLifecycleUnavailable(event));
148
+ } finally {
149
+ clearTimeout(abort);
150
+ clearTimeout(deadline);
151
+ }
152
+ }
153
+
154
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();
@@ -0,0 +1,20 @@
1
+ import { dirname, join } from 'node:path';
2
+ import { quoteShellArgument } from '../../cli/shell-argument.js';
3
+
4
+ export const CLAUDE_LIFECYCLE_MARKER = '--fuli-claude-lifecycle';
5
+
6
+ export function claudeLifecycleCommand(context, event, timeoutSec) {
7
+ const script = context.claudeLifecycleHookPath ??
8
+ join(dirname(context.mcpServerPath), 'agents', 'claude-code', 'lifecycle-hook.js');
9
+ return [
10
+ context.nodePath, script, CLAUDE_LIFECYCLE_MARKER,
11
+ '--runtime-config', context.runtimeConfigPath, '--event', event,
12
+ '--timeout-ms', String(Math.max(100, timeoutSec * 1000 - 2000))
13
+ ].map(value => quoteShellArgument(value, context.platform)).join(' ');
14
+ }
15
+
16
+ export function isClaudeLifecycleCommand(hook) {
17
+ return hook?.type === 'command' && typeof hook.command === 'string' &&
18
+ hook.command.includes(CLAUDE_LIFECYCLE_MARKER) &&
19
+ hook.command.includes('lifecycle-hook.js');
20
+ }
@@ -0,0 +1,22 @@
1
+ // Codex rollout v1: visible response items only; event_msg mirrors are omitted.
2
+ export function normalizeCodexRecord(record) {
3
+ if (record?.type !== 'response_item') return [];
4
+ const item = record.payload;
5
+ if (item?.type === 'message' && ['user', 'assistant'].includes(item.role)) {
6
+ const content = (item.content ?? []).filter(block => ['input_text', 'output_text'].includes(block.type))
7
+ .map(block => block.text ?? '').join('\n');
8
+ return content ? [{ role: item.role, kind: 'message', content }] : [];
9
+ }
10
+ if (['function_call', 'custom_tool_call'].includes(item?.type)) {
11
+ return [{ role: 'assistant', kind: 'tool_call', content: JSON.stringify({
12
+ call_id: item.call_id, name: item.name, arguments: item.arguments ?? item.input
13
+ }) }];
14
+ }
15
+ if (['function_call_output', 'custom_tool_call_output'].includes(item?.type)) {
16
+ return [{ role: 'tool', kind: 'tool_result', content: JSON.stringify({ call_id: item.call_id, output: item.output }) }];
17
+ }
18
+ return [];
19
+ }
20
+ export function verifyCodexTranscript(firstRecord, sessionId) {
21
+ return firstRecord?.type === 'session_meta' && firstRecord.payload?.id === sessionId;
22
+ }
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import { withTranscriptGuard } from '../../conversations/transcript-guard.js';
3
+ import { compactTaskContext } from '../../conversations/task-context-view.js';
4
+ import { syncConversationTranscript, withTranscriptNotice, claimTranscriptBoundary, recordTranscriptTaskEntry } from '../../conversations/sync-transcript.js';
5
+ import { normalizeCodexRecord, verifyCodexTranscript } from './conversation-transcript.js';
6
+ import { hookAdditionalContextToolResult } from '../../mcp/tool-result.js';
2
7
  import { resolve } from 'node:path';
3
8
  import { fileURLToPath } from 'node:url';
4
9
 
@@ -37,13 +42,26 @@ export async function codexStopLifecycleOutput(input, invoke) {
37
42
  return { decision: 'block', reason: boundedHookMessage(check.reason) };
38
43
  }
39
44
 
45
+ export async function codexLifecycleOutput(event, input, invoke) {
46
+ if (event === 'Stop') return codexStopLifecycleOutput(input, invoke);
47
+ if (!input?.session_id || typeof input.cwd !== 'string' || typeof input.prompt !== 'string') {
48
+ throw new TypeError('Codex task entry requires session, cwd and prompt');
49
+ }
50
+ const context = await invoke('begin_task_context', { sessionId: input.session_id,
51
+ turnId: input.turn_id ?? null, projectPath: input.cwd, taskPrompt: input.prompt.slice(0, 8192),
52
+ sourceApplication: 'codex', sourceSessionId: input.session_id });
53
+ return JSON.parse(hookAdditionalContextToolResult(compactTaskContext(context), { hookEventName: event,
54
+ label: 'Fuli task context. Apply effective_preferences and checkpoint with taskContextToken.',
55
+ limitBytes: 128 * 1024, itemLimit: 1000 }).content[0].text);
56
+ }
57
+
40
58
  export async function runCodexLifecycleHook(args, dependencies = {}) {
41
59
  const event = args[args.indexOf('--event') + 1];
42
- if (event !== 'Stop') throw new TypeError('Missing Codex lifecycle event');
60
+ if (!['Stop', 'UserPromptSubmit'].includes(event)) throw new TypeError('Missing Codex lifecycle event');
43
61
  const input = await (dependencies.readInput ?? readHookInput)();
44
62
  const write = dependencies.write ?? ((value) => process.stdout.write(value));
45
63
  if (dependencies.invoke) {
46
- const output = await codexStopLifecycleOutput(input, dependencies.invoke);
64
+ const output = await codexLifecycleOutput(event, input, dependencies.invoke);
47
65
  write(`${JSON.stringify(output)}\n`);
48
66
  return output;
49
67
  }
@@ -63,12 +81,33 @@ export async function runCodexLifecycleHook(args, dependencies = {}) {
63
81
  runtimeConfigPath
64
82
  });
65
83
  const invokeTool = dependencies.callTool ?? callAgentTool;
66
- const output = await leases.withGraphLease('codex-lifecycle', () =>
67
- codexStopLifecycleOutput(
68
- input,
69
- (name, parameters) => invokeTool(app, name, parameters)
70
- )
71
- );
84
+ const output = await leases.withGraphLease('codex-lifecycle', async () => {
85
+ try {
86
+ return await withTranscriptGuard(runtimeConfigPath, input, 'codex', app.getCapturePolicy?.().enabled, async guard => {
87
+ let sync = await syncConversationTranscript(app, input, 'codex', {
88
+ normalize: normalizeCodexRecord, verify: verifyCodexTranscript
89
+ }, guard);
90
+ if (event === 'UserPromptSubmit' && sync.entryBlocked) return { decision: 'block', reason: sync.reason };
91
+ const entryGuard = sync.status === 'capture_disabled' ? null : guard;
92
+ if (event === 'UserPromptSubmit' && entryGuard?.value) await entryGuard.write({ ...entryGuard.value, phase: 'beginning' });
93
+ const result = await codexLifecycleOutput(event, input, async (name, parameters) => {
94
+ const context = await invokeTool(app, name, parameters);
95
+ if (name === 'begin_task_context') await recordTranscriptTaskEntry(entryGuard, context);
96
+ return context;
97
+ });
98
+ if (event === 'UserPromptSubmit') sync = await claimTranscriptBoundary(app, input, 'codex', sync, {
99
+ normalize: normalizeCodexRecord, verify: verifyCodexTranscript
100
+ }, entryGuard);
101
+ if (event === 'UserPromptSubmit' && sync.entryBlocked) return { decision: 'block', reason: sync.reason };
102
+ return withTranscriptNotice(result, sync);
103
+ });
104
+ } catch (error) {
105
+ if (event !== 'UserPromptSubmit' || !app.getCapturePolicy?.().enabled || !input.transcript_path) throw error;
106
+ return { decision: 'block', reason: error.code === 'EADDRINUSE'
107
+ ? 'The session handoff lock is occupied. Retry after the current hook finishes; if it persists, start a new host session and resume the intended Agent.'
108
+ : 'Fuli transcript handoff could not be completed safely. Retry task entry; no previous Agent context was supplied.' };
109
+ }
110
+ });
72
111
  write(`${JSON.stringify(output)}\n`);
73
112
  return output;
74
113
  } finally {
@@ -18,8 +18,8 @@ export function withCodexLifecycleHooks(current, { timeout = 30, context = null
18
18
  }],
19
19
  ['Stop', 'verify_task_checkpoint', { sessionId: '${session_id}' }]
20
20
  ]) {
21
- const handler = event === 'Stop' && context
22
- ? { type: 'command', command: codexStopCommand(context), timeout }
21
+ const handler = context
22
+ ? { type: 'command', command: codexStopCommand(context, event), timeout }
23
23
  : { type: 'mcp_tool', server: 'fuli', tool, input, timeout };
24
24
  hooks[event] = [...(hooks[event] ?? []), { hooks: [handler] }];
25
25
  }
@@ -88,12 +88,12 @@ export function withCodexTomlLifecycleHooks(source, { timeout = 30, context = nu
88
88
  const lines = clean.replaceAll('\r\n', '\n').split('\n');
89
89
  while (lines.length && !lines.at(-1).trim()) lines.pop();
90
90
  const blocks = [
91
- ...codexTomlHookBlock('UserPromptSubmit', 'begin_task_context', {
91
+ ...(context ? codexTomlCommandHookBlock('UserPromptSubmit', codexStopCommand(context, 'UserPromptSubmit'), timeout) : codexTomlHookBlock('UserPromptSubmit', 'begin_task_context', {
92
92
  sessionId: '${session_id}',
93
93
  turnId: '${turn_id}',
94
94
  projectPath: '${cwd}',
95
95
  taskPrompt: '${prompt}'
96
- }, timeout),
96
+ }, timeout)),
97
97
  ...(context
98
98
  ? codexTomlCommandHookBlock('Stop', codexStopCommand(context), timeout)
99
99
  : codexTomlHookBlock('Stop', 'verify_task_checkpoint', {
@@ -212,7 +212,7 @@ function isManagedCodexHook(hook) {
212
212
  hook.command.includes(COMMAND_MARKER) && hook.command.includes('lifecycle-hook.js');
213
213
  }
214
214
 
215
- function codexStopCommand(context) {
215
+ function codexStopCommand(context, event = 'Stop') {
216
216
  const script = context.codexLifecycleHookPath ??
217
217
  join(dirname(context.mcpServerPath), 'agents', 'codex', 'lifecycle-hook.js');
218
218
  return [
@@ -222,7 +222,7 @@ function codexStopCommand(context) {
222
222
  '--runtime-config',
223
223
  context.runtimeConfigPath,
224
224
  '--event',
225
- 'Stop'
225
+ event
226
226
  ].map(value => quoteShellArgument(value, context.platform)).join(' ');
227
227
  }
228
228